/* Copyright 2004-2005 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.grails.orm.hibernate.cfg; import groovy.lang.Closure; import groovy.transform.Trait; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.transform.trait.Traits; import org.grails.datastore.mapping.core.connections.ConnectionSource; import org.grails.datastore.mapping.core.connections.ConnectionSourcesSupport; import org.grails.datastore.mapping.model.*; import org.grails.datastore.mapping.model.config.GormProperties; import org.grails.datastore.mapping.model.types.*; import org.grails.datastore.mapping.model.types.ToOne; import org.grails.datastore.mapping.reflect.EntityReflector; import org.grails.datastore.mapping.reflect.NameUtils; import org.grails.orm.hibernate.access.TraitPropertyAccessStrategy; import org.grails.orm.hibernate.proxy.GroovyAwarePojoEntityTuplizer; import org.hibernate.EntityMode; import org.hibernate.FetchMode; import org.hibernate.MappingException; import org.hibernate.annotations.common.reflection.java.JavaReflectionManager; import org.hibernate.boot.internal.ClassLoaderAccessImpl; import org.hibernate.boot.internal.MetadataBuildingContextRootImpl; import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.spi.*; import org.hibernate.cfg.*; import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.spi.FilterDefinition; import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.id.PersistentIdentifierGenerator; import org.hibernate.id.enhanced.SequenceStyleGenerator; import org.hibernate.mapping.*; import org.hibernate.mapping.Collection; import org.hibernate.mapping.ManyToOne; import org.hibernate.mapping.OneToMany; import org.hibernate.mapping.OneToOne; import org.hibernate.mapping.Table; import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.type.*; import org.jboss.jandex.IndexView; import org.springframework.util.StringUtils; import javax.persistence.Entity; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.Types; import java.util.*; import java.util.List; import java.util.Map; import java.util.Set; /** * Handles the binding Grails domain classes and properties to the Hibernate runtime meta model. * Based on the HbmBinder code in Hibernate core and influenced by AnnotationsBinder. * * @author Graeme Rocher * @since 0.1 */ public class GrailsDomainBinder implements MetadataContributor { protected static final String CASCADE_ALL_DELETE_ORPHAN = "all-delete-orphan"; protected static final String FOREIGN_KEY_SUFFIX = "_id"; protected static final String STRING_TYPE = "string"; protected static final String EMPTY_PATH = ""; protected static final char UNDERSCORE = '_'; protected static final String CASCADE_ALL = "all"; protected static final String CASCADE_SAVE_UPDATE = "save-update"; protected static final String CASCADE_NONE = "none"; protected static final String BACKTICK = "`"; protected static final String ENUM_TYPE_CLASS = "org.hibernate.type.EnumType"; protected static final String ENUM_CLASS_PROP = "enumClass"; protected static final String ENUM_TYPE_PROP = "type"; protected static final String DEFAULT_ENUM_TYPE = "default"; protected static final Log LOG = LogFactory.getLog(GrailsDomainBinder.class); public static final String SEQUENCE_KEY = "sequence"; /** * Overrideable naming strategy. Defaults to <code>ImprovedNamingStrategy</code> but can * be configured in DataSource.groovy via <code>hibernate.naming_strategy = ...</code>. */ public static Map<String, NamingStrategy> NAMING_STRATEGIES = new HashMap<>(); static { NAMING_STRATEGIES.put(ConnectionSource.DEFAULT, ImprovedNamingStrategy.INSTANCE); } protected final CollectionType CT = new CollectionType(null, this) { public Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { return null; } }; protected final String sessionFactoryName; protected final String dataSourceName; protected final HibernateMappingContext hibernateMappingContext; protected Closure defaultMapping; protected PersistentEntityNamingStrategy namingStrategy; protected MetadataBuildingContext metadataBuildingContext; public GrailsDomainBinder(String dataSourceName, String sessionFactoryName, HibernateMappingContext hibernateMappingContext) { this.sessionFactoryName = sessionFactoryName; this.dataSourceName = dataSourceName; this.hibernateMappingContext = hibernateMappingContext; // pre-build mappings for (PersistentEntity persistentEntity : hibernateMappingContext.getPersistentEntities()) { evaluateMapping(persistentEntity); } } /** * The default mapping defined by {@link org.grails.datastore.mapping.config.Settings#SETTING_DEFAULT_MAPPING} * @param defaultMapping The default mapping */ public void setDefaultMapping(Closure defaultMapping) { this.defaultMapping = defaultMapping; } /** * * @param namingStrategy Custom naming strategy to plugin into table naming */ public void setNamingStrategy(PersistentEntityNamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } @Override public void contribute(InFlightMetadataCollector metadataCollector, IndexView jandexIndex) { MetadataBuildingOptions options = metadataCollector.getMetadataBuildingOptions(); ClassLoaderService classLoaderService = options.getServiceRegistry().getService(ClassLoaderService.class); final ClassLoaderAccess classLoaderAccess = new ClassLoaderAccessImpl( options.getTempClassLoader(), classLoaderService ); this.metadataBuildingContext = new MetadataBuildingContextRootImpl( options, classLoaderAccess, metadataCollector ); java.util.Collection<PersistentEntity> persistentEntities = hibernateMappingContext.getPersistentEntities(); for (PersistentEntity persistentEntity : persistentEntities) { if(!persistentEntity.getJavaClass().isAnnotationPresent(Entity.class)) { if(ConnectionSourcesSupport.usesConnectionSource(persistentEntity, dataSourceName) && persistentEntity.isRoot()) { bindRoot((HibernatePersistentEntity) persistentEntity, metadataCollector, sessionFactoryName); } } } } /** * Override the default naming strategy for the default datasource given a Class or a full class name. * @param strategy the class or name * @throws ClassNotFoundException When the class was not found for specified strategy * @throws InstantiationException When an error occurred instantiating the strategy * @throws IllegalAccessException When an error occurred instantiating the strategy */ public static void configureNamingStrategy(final Object strategy) throws ClassNotFoundException, InstantiationException, IllegalAccessException { configureNamingStrategy(ConnectionSource.DEFAULT, strategy); } /** * Override the default naming strategy given a Class or a full class name, * or an instance of a NamingStrategy. * * @param datasourceName the datasource name * @param strategy the class, name, or instance * @throws ClassNotFoundException When the class was not found for specified strategy * @throws InstantiationException When an error occurred instantiating the strategy * @throws IllegalAccessException When an error occurred instantiating the strategy */ public static void configureNamingStrategy(final String datasourceName, final Object strategy) throws ClassNotFoundException, InstantiationException, IllegalAccessException { Class<?> namingStrategyClass = null; NamingStrategy namingStrategy; if (strategy instanceof Class<?>) { namingStrategyClass = (Class<?>)strategy; } else if (strategy instanceof CharSequence) { namingStrategyClass = Thread.currentThread().getContextClassLoader().loadClass(strategy.toString()); } if (namingStrategyClass == null) { namingStrategy = (NamingStrategy)strategy; } else { namingStrategy = (NamingStrategy)namingStrategyClass.newInstance(); } NAMING_STRATEGIES.put(datasourceName, namingStrategy); } protected void bindMapSecondPass(ToMany property, InFlightMetadataCollector mappings, Map<?, ?> persistentClasses, org.hibernate.mapping.Map map, String sessionFactoryBeanName) { bindCollectionSecondPass(property, mappings, persistentClasses, map, sessionFactoryBeanName); SimpleValue value = new SimpleValue(mappings, map.getCollectionTable()); bindSimpleValue(getIndexColumnType(property, STRING_TYPE), value, true, getIndexColumnName(property, sessionFactoryBeanName), mappings); PropertyConfig pc = getPropertyConfig(property); if (pc != null && pc.getIndexColumn() != null) { bindColumnConfigToColumn(property, getColumnForSimpleValue(value), getSingleColumnConfig(pc.getIndexColumn())); } if (!value.isTypeSpecified()) { throw new MappingException("map index element must specify a type: " + map.getRole()); } map.setIndex(value); if(!(property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !(property instanceof ManyToMany)) { SimpleValue elt = new SimpleValue(mappings, map.getCollectionTable()); map.setElement(elt); String typeName = getTypeName(property,getPropertyConfig(property), getMapping(property.getOwner())); if (typeName == null ) { if(property instanceof Basic) { Basic basic = (Basic) property; typeName = basic.getComponentType().getName(); } } if(typeName == null || typeName.equals(Object.class.getName())) { typeName = StandardBasicTypes.STRING.getName(); } bindSimpleValue(typeName, elt, false, getMapElementName(property, sessionFactoryBeanName), mappings); elt.setTypeName(typeName); } map.setInverse(false); } protected ColumnConfig getSingleColumnConfig(PropertyConfig propertyConfig) { if (propertyConfig != null) { List<ColumnConfig> columns = propertyConfig.getColumns(); if (columns != null && !columns.isEmpty()) { return columns.get(0); } } return null; } protected void bindListSecondPass(ToMany property, InFlightMetadataCollector mappings, Map<?, ?> persistentClasses, org.hibernate.mapping.List list, String sessionFactoryBeanName) { bindCollectionSecondPass(property, mappings, persistentClasses, list, sessionFactoryBeanName); String columnName = getIndexColumnName(property, sessionFactoryBeanName); final boolean isManyToMany = property instanceof ManyToMany; if (isManyToMany && !property.isOwningSide()) { throw new MappingException("Invalid association [" + property + "]. List collection types only supported on the owning side of a many-to-many relationship."); } Table collectionTable = list.getCollectionTable(); SimpleValue iv = new SimpleValue(mappings, collectionTable); bindSimpleValue("integer", iv, true, columnName, mappings); iv.setTypeName("integer"); list.setIndex(iv); list.setBaseIndex(0); list.setInverse(false); Value v = list.getElement(); v.createForeignKey(); if (property.isBidirectional()) { String entityName; Value element = list.getElement(); if (element instanceof ManyToOne) { ManyToOne manyToOne = (ManyToOne) element; entityName = manyToOne.getReferencedEntityName(); } else { entityName = ((OneToMany) element).getReferencedEntityName(); } PersistentClass referenced = mappings.getEntityBinding(entityName); Class<?> mappedClass = referenced.getMappedClass(); Mapping m = getMapping(mappedClass); boolean compositeIdProperty = isCompositeIdProperty(m, property.getInverseSide()); if (!compositeIdProperty) { Backref prop = new Backref(); final PersistentEntity owner = property.getOwner(); prop.setEntityName(owner.getName()); prop.setName(UNDERSCORE + addUnderscore(owner.getJavaClass().getSimpleName(), property.getName()) + "Backref"); prop.setSelectable(false); prop.setUpdateable(false); if (isManyToMany) { prop.setInsertable(false); } prop.setCollectionRole(list.getRole()); prop.setValue(list.getKey()); DependantValue value = (DependantValue) prop.getValue(); if (!property.isCircular()) { value.setNullable(false); } value.setUpdateable(true); prop.setOptional(false); referenced.addProperty(prop); } if ((!list.getKey().isNullable() && !list.isInverse()) || compositeIdProperty) { IndexBackref ib = new IndexBackref(); ib.setName(UNDERSCORE + property.getName() + "IndexBackref"); ib.setUpdateable(false); ib.setSelectable(false); if (isManyToMany) { ib.setInsertable(false); } ib.setCollectionRole(list.getRole()); ib.setEntityName(list.getOwner().getEntityName()); ib.setValue(list.getIndex()); referenced.addProperty(ib); } } } protected void bindCollectionSecondPass(ToMany property, InFlightMetadataCollector mappings, Map<?, ?> persistentClasses, Collection collection, String sessionFactoryBeanName) { PersistentClass associatedClass = null; if (LOG.isDebugEnabled()) LOG.debug("Mapping collection: " + collection.getRole() + " -> " + collection.getCollectionTable().getName()); PropertyConfig propConfig = getPropertyConfig(property); PersistentEntity referenced = property.getAssociatedEntity(); if (propConfig != null && StringUtils.hasText(propConfig.getSort())) { if (!property.isBidirectional() && (property instanceof org.grails.datastore.mapping.model.types.OneToMany)) { throw new DatastoreConfigurationException("Default sort for associations ["+property.getOwner().getName()+"->" + property.getName() + "] are not supported with unidirectional one to many relationships."); } if (referenced != null) { PersistentProperty propertyToSortBy = referenced.getPropertyByName(propConfig.getSort()); String associatedClassName = property.getAssociatedEntity().getName(); associatedClass = (PersistentClass) persistentClasses.get(associatedClassName); if (associatedClass != null) { collection.setOrderBy(buildOrderByClause(propertyToSortBy.getName(), associatedClass, collection.getRole(), propConfig.getOrder() != null ? propConfig.getOrder() : "asc")); } } } // Configure one-to-many if (collection.isOneToMany()) { Mapping m = getRootMapping(referenced); boolean tablePerSubclass = m != null && !m.getTablePerHierarchy(); if (referenced != null && !referenced.isRoot() && !tablePerSubclass) { Mapping rootMapping = getRootMapping(referenced); String discriminatorColumnName = RootClass.DEFAULT_DISCRIMINATOR_COLUMN_NAME; if (rootMapping != null) { DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); if(discriminatorConfig != null) { final ColumnConfig discriminatorColumn = discriminatorConfig.getColumn(); if (discriminatorColumn != null) { discriminatorColumnName = discriminatorColumn.getName(); } if (discriminatorConfig.getFormula() != null) { discriminatorColumnName = discriminatorConfig.getFormula(); } } } //NOTE: this will build the set for the in clause if it has sublcasses Set<String> discSet = buildDiscriminatorSet((HibernatePersistentEntity) referenced); String inclause = DefaultGroovyMethods.join(discSet, ","); collection.setWhere(discriminatorColumnName + " in (" + inclause + ")"); } OneToMany oneToMany = (OneToMany) collection.getElement(); String associatedClassName = oneToMany.getReferencedEntityName(); associatedClass = (PersistentClass) persistentClasses.get(associatedClassName); // if there is no persistent class for the association throw exception if (associatedClass == null) { throw new MappingException("Association references unmapped class: " + oneToMany.getReferencedEntityName()); } oneToMany.setAssociatedClass(associatedClass); if (shouldBindCollectionWithForeignKey(property)) { collection.setCollectionTable(associatedClass.getTable()); } bindCollectionForPropertyConfig(collection, propConfig); } if(referenced != null && referenced.isMultiTenant()) { String filterCondition = getMultiTenantFilterCondition(sessionFactoryBeanName, referenced); if(filterCondition != null) { collection.addFilter(GormProperties.TENANT_IDENTITY, filterCondition, true, Collections.<String, String>emptyMap(), Collections.<String, String>emptyMap()); } } if (isSorted(property)) { collection.setSorted(true); } // setup the primary key references DependantValue key = createPrimaryKeyValue(mappings, property, collection, persistentClasses); // link a bidirectional relationship if (property.isBidirectional()) { Association otherSide = property.getInverseSide(); if ((otherSide instanceof org.grails.datastore.mapping.model.types.ToOne) && shouldBindCollectionWithForeignKey(property)) { linkBidirectionalOneToMany(collection, associatedClass, key, otherSide); } else if ((otherSide instanceof ManyToMany) || Map.class.isAssignableFrom(property.getType())) { bindDependentKeyValue(property, key, mappings, sessionFactoryBeanName); } } else { if (hasJoinKeyMapping(propConfig)) { bindSimpleValue("long", key,false, propConfig.getJoinTable().getKey().getName(), mappings); } else { bindDependentKeyValue(property, key, mappings, sessionFactoryBeanName); } } collection.setKey(key); // get cache config if (propConfig != null) { CacheConfig cacheConfig = propConfig.getCache(); if (cacheConfig != null) { collection.setCacheConcurrencyStrategy(cacheConfig.getUsage()); } } // if we have a many-to-many final boolean isManyToMany = property instanceof ManyToMany; if (isManyToMany || isBidirectionalOneToManyMap(property)) { PersistentProperty otherSide = property.getInverseSide(); if (property.isBidirectional()) { if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] Mapping other side " + otherSide.getOwner().getName() + "." + otherSide.getName() + " -> " + collection.getCollectionTable().getName() + " as ManyToOne"); ManyToOne element = new ManyToOne(mappings, collection.getCollectionTable()); bindManyToMany((Association)otherSide, element, mappings, sessionFactoryBeanName); collection.setElement(element); bindCollectionForPropertyConfig(collection, propConfig); if (property.isCircular()) { collection.setInverse(false); } } else { // TODO support unidirectional many-to-many } } else if (shouldCollectionBindWithJoinColumn(property)) { bindCollectionWithJoinTable(property, mappings, collection, propConfig, sessionFactoryBeanName); } else if (isUnidirectionalOneToMany(property)) { // for non-inverse one-to-many, with a not-null fk, add a backref! // there are problems with list and map mappings and join columns relating to duplicate key constraints // TODO change this when HHH-1268 is resolved bindUnidirectionalOneToMany((org.grails.datastore.mapping.model.types.OneToMany) property, mappings, collection); } } private String getMultiTenantFilterCondition(String sessionFactoryBeanName, PersistentEntity referenced) { TenantId tenantId = referenced.getTenantId(); if(tenantId != null) { String defaultColumnName = getDefaultColumnName(tenantId, sessionFactoryBeanName); return ":tenantId = " + defaultColumnName; } else { return null; } } @SuppressWarnings("unchecked") protected String buildOrderByClause(String hqlOrderBy, PersistentClass associatedClass, String role, String defaultOrder) { String orderByString = null; if (hqlOrderBy != null) { List<String> properties = new ArrayList<String>(); List<String> ordering = new ArrayList<String>(); StringBuilder orderByBuffer = new StringBuilder(); if (hqlOrderBy.length() == 0) { //order by id Iterator<?> it = associatedClass.getIdentifier().getColumnIterator(); while (it.hasNext()) { Selectable col = (Selectable) it.next(); orderByBuffer.append(col.getText()).append(" asc").append(", "); } } else { StringTokenizer st = new StringTokenizer(hqlOrderBy, " ,", false); String currentOrdering = defaultOrder; //FIXME make this code decent while (st.hasMoreTokens()) { String token = st.nextToken(); if (isNonPropertyToken(token)) { if (currentOrdering != null) { throw new DatastoreConfigurationException( "Error while parsing sort clause: " + hqlOrderBy + " (" + role + ")" ); } currentOrdering = token; } else { //Add ordering of the previous if (currentOrdering == null) { //default ordering ordering.add("asc"); } else { ordering.add(currentOrdering); currentOrdering = null; } properties.add(token); } } ordering.remove(0); //first one is the algorithm starter // add last one ordering if (currentOrdering == null) { //default ordering ordering.add(defaultOrder); } else { ordering.add(currentOrdering); currentOrdering = null; } int index = 0; for (String property : properties) { Property p = BinderHelper.findPropertyByName(associatedClass, property); if (p == null) { throw new DatastoreConfigurationException( "property from sort clause not found: " + associatedClass.getEntityName() + "." + property ); } PersistentClass pc = p.getPersistentClass(); String table; if (pc == null) { table = ""; } else if (pc == associatedClass || (associatedClass instanceof SingleTableSubclass && pc.getMappedClass().isAssignableFrom(associatedClass.getMappedClass()))) { table = ""; } else { table = pc.getTable().getQuotedName() + "."; } Iterator<?> propertyColumns = p.getColumnIterator(); while (propertyColumns.hasNext()) { Selectable column = (Selectable) propertyColumns.next(); orderByBuffer.append(table) .append(column.getText()) .append(" ") .append(ordering.get(index)) .append(", "); } index++; } } orderByString = orderByBuffer.substring(0, orderByBuffer.length() - 2); } return orderByString; } protected boolean isNonPropertyToken(String token) { if (" ".equals(token)) return true; if (",".equals(token)) return true; if (token.equalsIgnoreCase("desc")) return true; if (token.equalsIgnoreCase("asc")) return true; return false; } protected Set<String> buildDiscriminatorSet(HibernatePersistentEntity domainClass) { Set<String> theSet = new HashSet<String>(); Mapping mapping = domainClass.getMapping().getMappedForm(); String discriminator = domainClass.getName(); if (mapping != null && mapping.getDiscriminator() != null) { DiscriminatorConfig discriminatorConfig = mapping.getDiscriminator(); if(discriminatorConfig.getValue() != null) { discriminator = discriminatorConfig.getValue(); } } Mapping rootMapping = getRootMapping(domainClass); String quote = "'"; if (rootMapping != null && rootMapping.getDatasources() != null) { DiscriminatorConfig discriminatorConfig = rootMapping.getDiscriminator(); if(discriminatorConfig != null && discriminatorConfig.getType() != null && !discriminatorConfig.getType().equals("string")) quote = ""; } theSet.add(quote + discriminator + quote); final java.util.Collection<PersistentEntity> childEntities = domainClass.getMappingContext().getDirectChildEntities(domainClass); for (PersistentEntity subClass : childEntities) { theSet.addAll(buildDiscriminatorSet((HibernatePersistentEntity) subClass)); } return theSet; } protected Mapping getRootMapping(PersistentEntity referenced) { if (referenced == null) return null; Class<?> current = referenced.getJavaClass(); while (true) { Class<?> superClass = current.getSuperclass(); if (Object.class.equals(superClass)) break; current = superClass; } return getMapping(current); } protected boolean isBidirectionalOneToManyMap(Association property) { return Map.class.isAssignableFrom(property.getType()) && property.isBidirectional(); } protected void bindCollectionWithJoinTable(ToMany property, InFlightMetadataCollector mappings, Collection collection, PropertyConfig config, String sessionFactoryBeanName) { NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); SimpleValue element; final boolean isBasicCollectionType = property instanceof Basic; if (isBasicCollectionType) { element = new SimpleValue(mappings, collection.getCollectionTable()); } else { // for a normal unidirectional one-to-many we use a join column element = new ManyToOne(mappings, collection.getCollectionTable()); bindUnidirectionalOneToManyInverseValues(property, (ManyToOne) element); } collection.setInverse(false); String columnName; final boolean hasJoinColumnMapping = hasJoinColumnMapping(config); if (isBasicCollectionType) { final Class<?> referencedType = ((Basic)property).getComponentType(); String className = referencedType.getName(); final boolean isEnum = referencedType.isEnum(); if (hasJoinColumnMapping) { columnName = config.getJoinTable().getColumn().getName(); } else { columnName = isEnum ? namingStrategy.propertyToColumnName(className) : addUnderscore(namingStrategy.propertyToColumnName(property.getName()), namingStrategy.propertyToColumnName(className)); } if (isEnum) { bindEnumType(property, referencedType,element,columnName); } else { String typeName = getTypeName(property, config, getMapping(property.getOwner())); if (typeName == null) { Type type = mappings.getTypeResolver().basic(className); if (type != null) { typeName = type.getName(); } } if (typeName == null) { String domainName = property.getOwner().getName(); throw new MappingException("Missing type or column for column["+columnName+"] on domain["+domainName+"] referencing["+className+"]"); } bindSimpleValue(typeName, element,true, columnName, mappings); if (hasJoinColumnMapping) { bindColumnConfigToColumn(property, getColumnForSimpleValue(element), config.getJoinTable().getColumn()); } } } else { final PersistentEntity domainClass = property.getAssociatedEntity(); Mapping m = getMapping(domainClass); if (hasCompositeIdentifier(m)) { CompositeIdentity ci = (CompositeIdentity) m.getIdentity(); bindCompositeIdentifierToManyToOne(property, element, ci, domainClass, EMPTY_PATH, sessionFactoryBeanName); } else { if (hasJoinColumnMapping) { columnName = config.getJoinTable().getColumn().getName(); } else { columnName = namingStrategy.propertyToColumnName(NameUtils.decapitalize(domainClass.getName())) + FOREIGN_KEY_SUFFIX; } bindSimpleValue("long", element,true, columnName, mappings); } } collection.setElement(element); bindCollectionForPropertyConfig(collection, config); } protected String addUnderscore(String s1, String s2) { return removeBackticks(s1) + UNDERSCORE + removeBackticks(s2); } protected String removeBackticks(String s) { return s.startsWith("`") && s.endsWith("`") ? s.substring(1, s.length() - 1) : s; } protected Column getColumnForSimpleValue(SimpleValue element) { return (Column)element.getColumnIterator().next(); } protected String getTypeName(PersistentProperty property, PropertyConfig config, Mapping mapping) { if (config != null && config.getType() != null) { final Object typeObj = config.getType(); if (typeObj instanceof Class<?>) { return ((Class<?>)typeObj).getName(); } return typeObj.toString(); } if (mapping != null) { return mapping.getTypeName(property.getType()); } return null; } protected void bindColumnConfigToColumn(PersistentProperty property, Column column, ColumnConfig columnConfig) { final PropertyConfig mappedForm = property != null ? (PropertyConfig) property.getMapping().getMappedForm() : null; boolean allowUnique = mappedForm != null && !mappedForm.isUniqueWithinGroup(); if (columnConfig == null) { return; } if (columnConfig.getLength() != -1) { column.setLength(columnConfig.getLength()); } if (columnConfig.getPrecision() != -1) { column.setPrecision(columnConfig.getPrecision()); } if (columnConfig.getScale() != -1) { column.setScale(columnConfig.getScale()); } if (columnConfig.getSqlType() != null && !columnConfig.getSqlType().isEmpty()) { column.setSqlType(columnConfig.getSqlType()); } if(allowUnique) { column.setUnique(columnConfig.getUnique()); } } protected boolean hasJoinColumnMapping(PropertyConfig config) { return config != null && config.getJoinTable() != null && config.getJoinTable().getColumn() != null; } protected boolean shouldCollectionBindWithJoinColumn(ToMany property) { PropertyConfig config = getPropertyConfig(property); JoinTable jt = config != null ? config.getJoinTable() : new JoinTable(); return (isUnidirectionalOneToMany(property) || (property instanceof Basic)) && jt != null; } /** * @param property The property to bind * @param manyToOne The inverse side */ protected void bindUnidirectionalOneToManyInverseValues(ToMany property, ManyToOne manyToOne) { PropertyConfig config = getPropertyConfig(property); if (config == null) { manyToOne.setLazy(true); } else { manyToOne.setIgnoreNotFound(config.getIgnoreNotFound()); final FetchMode fetch = config.getFetchMode(); if(!fetch.equals(FetchMode.JOIN) && !fetch.equals(FetchMode.EAGER)) { manyToOne.setLazy(true); } final Boolean lazy = config.getLazy(); if(lazy != null) { manyToOne.setLazy(lazy); } } // set referenced entity manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName()); } protected void bindCollectionForPropertyConfig(Collection collection, PropertyConfig config) { if (config == null) { collection.setLazy(true); collection.setExtraLazy(false); } else { final FetchMode fetch = config.getFetchMode(); if(!fetch.equals(FetchMode.JOIN) && !fetch.equals(FetchMode.EAGER)) { collection.setLazy(true); } final Boolean lazy = config.getLazy(); if(lazy != null) { collection.setExtraLazy(lazy); } } } public PropertyConfig getPropertyConfig(PersistentProperty property) { return (PropertyConfig) property.getMapping().getMappedForm(); } /** * Checks whether a property is a unidirectional non-circular one-to-many * * @param property The property to check * @return true if it is unidirectional and a one-to-many */ protected boolean isUnidirectionalOneToMany(PersistentProperty property) { return ((property instanceof org.grails.datastore.mapping.model.types.OneToMany) && !((Association)property).isBidirectional()); } /** * Binds the primary key value column * * @param property The property * @param key The key * @param mappings The mappings * @param sessionFactoryBeanName The name of the session factory */ protected void bindDependentKeyValue(PersistentProperty property, DependantValue key, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] binding [" + property.getName() + "] with dependant key"); } PersistentEntity refDomainClass = property.getOwner(); final Mapping mapping = getMapping(refDomainClass.getJavaClass()); boolean hasCompositeIdentifier = hasCompositeIdentifier(mapping); if ((shouldCollectionBindWithJoinColumn((ToMany) property) && hasCompositeIdentifier) || (hasCompositeIdentifier && ( property instanceof ManyToMany))) { CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); bindCompositeIdentifierToManyToOne((Association) property, key, ci, refDomainClass, EMPTY_PATH, sessionFactoryBeanName); } else { bindSimpleValue(property, null, key, EMPTY_PATH, mappings, sessionFactoryBeanName); } } /** * Creates the DependentValue object that forms a primary key reference for the collection. * * @param mappings * @param property The grails property * @param collection The collection object * @param persistentClasses * @return The DependantValue (key) */ protected DependantValue createPrimaryKeyValue(InFlightMetadataCollector mappings, PersistentProperty property, Collection collection, Map<?, ?> persistentClasses) { KeyValue keyValue; DependantValue key; String propertyRef = collection.getReferencedPropertyName(); // this is to support mapping by a property if (propertyRef == null) { keyValue = collection.getOwner().getIdentifier(); } else { keyValue = (KeyValue) collection.getOwner().getProperty(propertyRef).getValue(); } if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] creating dependant key value to table [" + keyValue.getTable().getName() + "]"); key = new DependantValue(mappings, collection.getCollectionTable(), keyValue); key.setTypeName(null); // make nullable and non-updateable key.setNullable(true); key.setUpdateable(false); return key; } /** * Binds a unidirectional one-to-many creating a psuedo back reference property in the process. * * @param property * @param mappings * @param collection */ protected void bindUnidirectionalOneToMany(org.grails.datastore.mapping.model.types.OneToMany property, InFlightMetadataCollector mappings, Collection collection) { Value v = collection.getElement(); v.createForeignKey(); String entityName; if (v instanceof ManyToOne) { ManyToOne manyToOne = (ManyToOne) v; entityName = manyToOne.getReferencedEntityName(); } else { entityName = ((OneToMany) v).getReferencedEntityName(); } collection.setInverse(false); PersistentClass referenced = mappings.getEntityBinding(entityName); Backref prop = new Backref(); PersistentEntity owner = property.getOwner(); prop.setEntityName(owner.getName()); prop.setName(UNDERSCORE + addUnderscore(owner.getJavaClass().getSimpleName(), property.getName()) + "Backref"); prop.setUpdateable(false); prop.setInsertable(true); prop.setCollectionRole(collection.getRole()); prop.setValue(collection.getKey()); prop.setOptional(true); referenced.addProperty(prop); } protected Property getProperty(PersistentClass associatedClass, String propertyName) throws MappingException { try { return associatedClass.getProperty(propertyName); } catch (MappingException e) { //maybe it's squirreled away in a composite primary key if (associatedClass.getKey() instanceof Component) { return ((Component) associatedClass.getKey()).getProperty(propertyName); } throw e; } } /** * Links a bidirectional one-to-many, configuring the inverse side and using a column copy to perform the link * * @param collection The collection one-to-many * @param associatedClass The associated class * @param key The key * @param otherSide The other side of the relationship */ protected void linkBidirectionalOneToMany(Collection collection, PersistentClass associatedClass, DependantValue key, PersistentProperty otherSide) { collection.setInverse(true); // Iterator mappedByColumns = associatedClass.getProperty(otherSide.getName()).getValue().getColumnIterator(); Iterator<?> mappedByColumns = getProperty(associatedClass, otherSide.getName()).getValue().getColumnIterator(); while (mappedByColumns.hasNext()) { Column column = (Column) mappedByColumns.next(); linkValueUsingAColumnCopy(otherSide, column, key); } } /** * Establish whether a collection property is sorted * * @param property The property * @return true if sorted */ protected boolean isSorted(PersistentProperty property) { return SortedSet.class.isAssignableFrom(property.getType()); } /** * Binds a many-to-many relationship. A many-to-many consists of * - a key (a DependentValue) * - an element * * The element is a ManyToOne from the association table to the target entity * * @param property The grails property * @param element The ManyToOne element * @param mappings The mappings */ protected void bindManyToMany(Association property, ManyToOne element, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { bindManyToOne(property, element, EMPTY_PATH, mappings, sessionFactoryBeanName); element.setReferencedEntityName(property.getOwner().getName()); } protected void linkValueUsingAColumnCopy(PersistentProperty prop, Column column, DependantValue key) { Column mappingColumn = new Column(); mappingColumn.setName(column.getName()); mappingColumn.setLength(column.getLength()); mappingColumn.setNullable(prop.isNullable()); mappingColumn.setSqlType(column.getSqlType()); mappingColumn.setValue(key); key.addColumn(mappingColumn); key.getTable().addColumn(mappingColumn); } /** * First pass to bind collection to Hibernate metamodel, sets up second pass * * @param property The GrailsDomainClassProperty instance * @param collection The collection * @param owner The owning persistent class * @param mappings The Hibernate mappings instance * @param path */ protected void bindCollection(ToMany property, Collection collection, PersistentClass owner, InFlightMetadataCollector mappings, String path, String sessionFactoryBeanName) { // set role String propertyName = getNameForPropertyAndPath(property, path); collection.setRole(qualify(property.getOwner().getName(), propertyName)); PropertyConfig pc = getPropertyConfig(property); // configure eager fetching final FetchMode fetchMode = pc.getFetchMode(); if (fetchMode == FetchMode.JOIN) { collection.setFetchMode(FetchMode.JOIN); } else if (pc.getFetchMode() != null) { collection.setFetchMode(pc.getFetchMode()); } else { collection.setFetchMode(FetchMode.DEFAULT); } if (pc.getCascade() != null) { collection.setOrphanDelete(pc.getCascade().equals(CASCADE_ALL_DELETE_ORPHAN)); } // if it's a one-to-many mapping if (shouldBindCollectionWithForeignKey(property)) { OneToMany oneToMany = new OneToMany(mappings, collection.getOwner()); collection.setElement(oneToMany); bindOneToMany((org.grails.datastore.mapping.model.types.OneToMany) property, oneToMany, mappings); } else { bindCollectionTable(property, mappings, collection, owner.getTable(), sessionFactoryBeanName); if (!property.isOwningSide()) { collection.setInverse(true); } } if (pc.getBatchSize() != null) { collection.setBatchSize(pc.getBatchSize()); } // set up second pass if (collection instanceof org.hibernate.mapping.Set) { mappings.addSecondPass(new GrailsCollectionSecondPass(property, mappings, collection, sessionFactoryBeanName)); } else if (collection instanceof org.hibernate.mapping.List) { mappings.addSecondPass(new ListSecondPass(property, mappings, collection, sessionFactoryBeanName)); } else if (collection instanceof org.hibernate.mapping.Map) { mappings.addSecondPass(new MapSecondPass(property, mappings, collection, sessionFactoryBeanName)); } else { // Collection -> Bag mappings.addSecondPass(new GrailsCollectionSecondPass(property, mappings, collection, sessionFactoryBeanName)); } } /* * We bind collections with foreign keys if specified in the mapping and only if * it is a unidirectional one-to-many that is. */ protected boolean shouldBindCollectionWithForeignKey(ToMany property) { return ((property instanceof org.grails.datastore.mapping.model.types.OneToMany) && property.isBidirectional() || !shouldCollectionBindWithJoinColumn(property)) && !Map.class.isAssignableFrom(property.getType()) && !(property instanceof ManyToMany) && !(property instanceof Basic); } protected String getNameForPropertyAndPath(PersistentProperty property, String path) { if (isNotEmpty(path)) { return qualify(path, property.getName()); } return property.getName(); } protected void bindCollectionTable(ToMany property, InFlightMetadataCollector mappings, Collection collection, Table ownerTable, String sessionFactoryBeanName) { String owningTableSchema = ownerTable.getSchema(); PropertyConfig config = getPropertyConfig(property); JoinTable jt = config != null ? config.getJoinTable() : null; NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); String tableName = (jt != null && jt.getName() != null ? jt.getName() : namingStrategy.tableName(calculateTableForMany(property, sessionFactoryBeanName))); String schemaName = getSchemaName(mappings); String catalogName = getCatalogName(mappings); if(jt != null) { if(jt.getSchema() != null) { schemaName = jt.getSchema(); } if(jt.getCatalog() != null) { catalogName = jt.getCatalog(); } } if(schemaName == null && owningTableSchema != null) { schemaName = owningTableSchema; } collection.setCollectionTable(mappings.addTable( schemaName, catalogName, tableName, null, false)); } /** * Calculates the mapping table for a many-to-many. One side of * the relationship has to "own" the relationship so that there is not a situation * where you have two mapping tables for left_right and right_left */ protected String calculateTableForMany(ToMany property, String sessionFactoryBeanName) { NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); String propertyColumnName = namingStrategy.propertyToColumnName(property.getName()); //fix for GRAILS-5895 PropertyConfig config = getPropertyConfig(property); JoinTable jt = config != null ? config.getJoinTable() : null; boolean hasJoinTableMapping = jt != null && jt.getName() != null; String left = getTableName(property.getOwner(), sessionFactoryBeanName); if (Map.class.isAssignableFrom(property.getType())) { if (hasJoinTableMapping) { return jt.getName(); } return addUnderscore(left, propertyColumnName); } if (property instanceof Basic) { if (hasJoinTableMapping) { return jt.getName(); } return addUnderscore(left, propertyColumnName); } String right = getTableName(property.getAssociatedEntity(), sessionFactoryBeanName); if (property instanceof ManyToMany) { if (hasJoinTableMapping) { return jt.getName(); } if (property.isOwningSide()) { return addUnderscore(left, propertyColumnName); } return addUnderscore(right, namingStrategy.propertyToColumnName(((ManyToMany) property).getInversePropertyName())); } if (shouldCollectionBindWithJoinColumn(property)) { if (hasJoinTableMapping) { return jt.getName(); } left = trimBackTigs(left); right = trimBackTigs(right); return addUnderscore(left, right); } if (property.isOwningSide()) { return addUnderscore(left, right); } return addUnderscore(right, left); } protected String trimBackTigs(String tableName) { if (tableName.startsWith(BACKTICK)) { return tableName.substring(1, tableName.length() - 1); } return tableName; } /** * Evaluates the table name for the given property * * @param domainClass The domain class to evaluate * @return The table name */ protected String getTableName(PersistentEntity domainClass, String sessionFactoryBeanName) { Mapping m = getMapping(domainClass); String tableName = null; if (m != null && m.getTableName() != null) { tableName = m.getTableName(); } if (tableName == null) { String shortName = domainClass.getJavaClass().getSimpleName(); PersistentEntityNamingStrategy namingStrategy = this.namingStrategy; if(namingStrategy != null) { tableName = namingStrategy.resolveTableName(domainClass); } if(tableName == null) { tableName = getNamingStrategy(sessionFactoryBeanName).classToTableName(shortName); } } return tableName; } protected NamingStrategy getNamingStrategy(String sessionFactoryBeanName) { String key = "sessionFactory".equals(sessionFactoryBeanName) ? Mapping.DEFAULT_DATA_SOURCE : sessionFactoryBeanName.substring("sessionFactory_".length()); NamingStrategy namingStrategy = NAMING_STRATEGIES.get(key); return namingStrategy != null ? namingStrategy : new ImprovedNamingStrategy(); } /** * Binds a Grails domain class to the Hibernate runtime meta model * * @param entity The domain class to bind * @param mappings The existing mappings * @param sessionFactoryBeanName the session factory bean name * @throws MappingException Thrown if the domain class uses inheritance which is not supported */ public void bindClass(PersistentEntity entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { //if (domainClass.getClazz().getSuperclass() == Object.class) { if (entity.isRoot()) { bindRoot((HibernatePersistentEntity) entity, mappings, sessionFactoryBeanName); } } /** * Evaluates a Mapping object from the domain class if it has a mapping closure * * @param domainClass The domain class * @return the mapping */ public Mapping evaluateMapping(PersistentEntity domainClass) { return evaluateMapping(domainClass, null); } public Mapping evaluateMapping(PersistentEntity domainClass, Closure<?> defaultMapping) { return evaluateMapping(domainClass, defaultMapping, true); } public Mapping evaluateMapping(PersistentEntity domainClass, Closure<?> defaultMapping, boolean cache) { try { final Mapping m = (Mapping) domainClass.getMapping().getMappedForm(); trackCustomCascadingSaves(m, domainClass.getPersistentProperties()); if (cache) { AbstractGrailsDomainBinder.cacheMapping(domainClass.getJavaClass(), m); } return m; } catch (Exception e) { throw new DatastoreConfigurationException("Error evaluating ORM mappings block for domain [" + domainClass.getName() + "]: " + e.getMessage(), e); } } /** * Checks for any custom cascading saves set up via the mapping DSL and records them within the persistent property. * @param mapping The Mapping. * @param persistentProperties The persistent properties of the domain class. */ protected void trackCustomCascadingSaves(Mapping mapping, Iterable<PersistentProperty> persistentProperties) { for (PersistentProperty property : persistentProperties) { PropertyConfig propConf = mapping.getPropertyConfig(property.getName()); if (propConf != null && propConf.getCascade() != null) { propConf.setExplicitSaveUpdateCascade(isSaveUpdateCascade(propConf.getCascade())); } } } /** * Check if a save-update cascade is defined within the Hibernate cascade properties string. * @param cascade The string containing the cascade properties. * @return True if save-update or any other cascade property that encompasses those is present. */ protected boolean isSaveUpdateCascade(String cascade) { String[] cascades = cascade.split(","); for (String cascadeProp : cascades) { String trimmedProp = cascadeProp.trim(); if (CASCADE_SAVE_UPDATE.equals(trimmedProp) || CASCADE_ALL.equals(trimmedProp) || CASCADE_ALL_DELETE_ORPHAN.equals(trimmedProp)) { return true; } } return false; } /** * Obtains a mapping object for the given domain class nam * * @param theClass The domain class in question * @return A Mapping object or null */ public static Mapping getMapping(Class<?> theClass) { return AbstractGrailsDomainBinder.getMapping(theClass); } /** * Obtains a mapping object for the given domain class nam * * @param domainClass The domain class in question * @return A Mapping object or null */ public static Mapping getMapping(PersistentEntity domainClass) { return domainClass == null ? null : AbstractGrailsDomainBinder.getMapping(domainClass.getJavaClass()); } public static void clearMappingCache() { AbstractGrailsDomainBinder.clearMappingCache(); } public static void clearMappingCache(Class<?> theClass) { // no-op, here for compatibility } /** * Binds the specified persistant class to the runtime model based on the * properties defined in the domain class * * @param domainClass The Grails domain class * @param persistentClass The persistant class * @param mappings Existing mappings */ protected void bindClass(PersistentEntity domainClass, PersistentClass persistentClass, InFlightMetadataCollector mappings) { // set lazy loading for now persistentClass.setLazy(true); final String entityName = domainClass.getName(); persistentClass.setEntityName(entityName); persistentClass.setJpaEntityName(unqualify(entityName)); persistentClass.setProxyInterfaceName(entityName); persistentClass.setClassName(entityName); persistentClass.addTuplizer(EntityMode.POJO, GroovyAwarePojoEntityTuplizer.class.getName()); // set dynamic insert to false persistentClass.setDynamicInsert(false); // set dynamic update to false persistentClass.setDynamicUpdate(false); // set select before update to false persistentClass.setSelectBeforeUpdate(false); // add import to mappings String en = persistentClass.getEntityName(); if (mappings.getMetadataBuildingOptions().getMappingDefaults().isAutoImportEnabled() && en.indexOf('.') > 0) { String unqualified = unqualify(en); mappings.addImport(unqualified, en); } } /** * Binds a root class (one with no super classes) to the runtime meta model * based on the supplied Grails domain class * * @param entity The Grails domain class * @param mappings The Hibernate Mappings object * @param sessionFactoryBeanName the session factory bean name */ public void bindRoot(HibernatePersistentEntity entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { if (mappings.getEntityBinding(entity.getName()) != null) { LOG.info("[GrailsDomainBinder] Class [" + entity.getName() + "] is already mapped, skipping.. "); return; } RootClass root = new RootClass(this.metadataBuildingContext); root.setAbstract(entity.isAbstract()); final MappingContext mappingContext = entity.getMappingContext(); final java.util.Collection<PersistentEntity> children = mappingContext.getDirectChildEntities(entity); if (children.isEmpty()) { root.setPolymorphic(false); } bindClass(entity, root, mappings); Mapping m = getMapping(entity); bindRootPersistentClassCommonValues(entity, root, mappings, sessionFactoryBeanName); if (!children.isEmpty()) { boolean tablePerSubclass = m != null && !m.getTablePerHierarchy(); if (!tablePerSubclass) { // if the root class has children create a discriminator property bindDiscriminatorProperty(root.getTable(), root, mappings); } // bind the sub classes bindSubClasses(entity, root, mappings, sessionFactoryBeanName); } if(entity.isMultiTenant()) { TenantId tenantId = entity.getTenantId(); if(tenantId != null) { String filterCondition = getMultiTenantFilterCondition(sessionFactoryBeanName, entity); root.addFilter(GormProperties.TENANT_IDENTITY,filterCondition, true, Collections.<String, String>emptyMap(), Collections.<String, String>emptyMap()); mappings.addFilterDefinition(new FilterDefinition( GormProperties.TENANT_IDENTITY, filterCondition, Collections.singletonMap(GormProperties.TENANT_IDENTITY, root.getProperty(tenantId.getName()).getType()) )); } } mappings.addEntityBinding(root); } /** * Binds the sub classes of a root class using table-per-heirarchy inheritance mapping * * @param domainClass The root domain class to bind * @param parent The parent class instance * @param mappings The mappings instance * @param sessionFactoryBeanName the session factory bean name */ protected void bindSubClasses(HibernatePersistentEntity domainClass, PersistentClass parent, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { final java.util.Collection<PersistentEntity> subClasses = domainClass.getMappingContext().getDirectChildEntities(domainClass); for (PersistentEntity sub : subClasses) { final Class javaClass = sub.getJavaClass(); if (javaClass.getSuperclass().equals(domainClass.getJavaClass())) { bindSubClass((HibernatePersistentEntity)sub, parent, mappings, sessionFactoryBeanName); } } } /** * Binds a sub class. * * @param sub The sub domain class instance * @param parent The parent persistent class instance * @param mappings The mappings instance * @param sessionFactoryBeanName the session factory bean name */ protected void bindSubClass(HibernatePersistentEntity sub, PersistentClass parent, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { evaluateMapping(sub, defaultMapping); Mapping m = getMapping(parent.getMappedClass()); Subclass subClass; boolean tablePerSubclass = m != null && !m.getTablePerHierarchy() && !m.isTablePerConcreteClass(); boolean tablePerConcreteClass = m != null && m.isTablePerConcreteClass(); final String fullName = sub.getName(); if (tablePerSubclass) { subClass = new JoinedSubclass( parent, this.metadataBuildingContext); } else if(tablePerConcreteClass) { subClass = new UnionSubclass(parent, this.metadataBuildingContext); } else { subClass = new SingleTableSubclass(parent, this.metadataBuildingContext); // set the descriminator value as the name of the class. This is the // value used by Hibernate to decide what the type of the class is // to perform polymorphic queries Mapping subMapping = getMapping(sub); DiscriminatorConfig discriminatorConfig = subMapping != null ? subMapping.getDiscriminator() : null; subClass.setDiscriminatorValue(discriminatorConfig != null && discriminatorConfig.getValue() != null ? discriminatorConfig.getValue() : fullName); if (subMapping != null) { configureDerivedProperties(sub, subMapping); } } subClass.setEntityName(fullName); subClass.setJpaEntityName(unqualify(fullName)); parent.addSubclass(subClass); mappings.addEntityBinding(subClass); if (tablePerSubclass) { bindJoinedSubClass(sub, (JoinedSubclass) subClass, mappings, m, sessionFactoryBeanName); } else if( tablePerConcreteClass) { bindUnionSubclass(sub, (UnionSubclass) subClass, mappings, sessionFactoryBeanName); } else { bindSubClass(sub, subClass, mappings, sessionFactoryBeanName); } final java.util.Collection<PersistentEntity> childEntities = sub.getMappingContext().getDirectChildEntities(sub); if (!childEntities.isEmpty()) { // bind the sub classes bindSubClasses(sub, subClass, mappings, sessionFactoryBeanName); } } public void bindUnionSubclass(HibernatePersistentEntity subClass, UnionSubclass unionSubclass, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { Mapping subMapping = getMapping(subClass.getJavaClass()); if ( unionSubclass.getEntityPersisterClass() == null ) { unionSubclass.getRootClass().setEntityPersisterClass( UnionSubclassEntityPersister.class ); } String schema = subMapping != null && subMapping.getTable().getSchema() != null ? subMapping.getTable().getSchema() : null; String catalog = subMapping != null && subMapping.getTable().getCatalog() != null ? subMapping.getTable().getCatalog() : null; Table denormalizedSuperTable = unionSubclass.getSuperclass().getTable(); Table mytable = mappings.addDenormalizedTable( schema, catalog, getTableName(subClass, sessionFactoryBeanName), unionSubclass.isAbstract() != null && unionSubclass.isAbstract(), null, denormalizedSuperTable ); unionSubclass.setTable( mytable ); unionSubclass.setClassName(subClass.getName()); LOG.info( "Mapping union-subclass: " + unionSubclass.getEntityName() + " -> " + unionSubclass.getTable().getName() ); createClassProperties(subClass, unionSubclass, mappings, sessionFactoryBeanName); } /** * Binds a joined sub-class mapping using table-per-subclass * * @param sub The Grails sub class * @param joinedSubclass The Hibernate Subclass object * @param mappings The mappings Object * @param gormMapping The GORM mapping object * @param sessionFactoryBeanName the session factory bean name */ protected void bindJoinedSubClass(HibernatePersistentEntity sub, JoinedSubclass joinedSubclass, InFlightMetadataCollector mappings, Mapping gormMapping, String sessionFactoryBeanName) { bindClass(sub, joinedSubclass, mappings); String schemaName = getSchemaName(mappings); String catalogName = getCatalogName(mappings); Table mytable = mappings.addTable( schemaName, catalogName, getJoinedSubClassTableName(sub, joinedSubclass, null, mappings, sessionFactoryBeanName), null, false); joinedSubclass.setTable(mytable); LOG.info("Mapping joined-subclass: " + joinedSubclass.getEntityName() + " -> " + joinedSubclass.getTable().getName()); SimpleValue key = new DependantValue(mappings, mytable, joinedSubclass.getIdentifier()); joinedSubclass.setKey(key); final PersistentProperty identifier = sub.getIdentity(); String columnName = getColumnNameForPropertyAndPath(identifier, EMPTY_PATH, null, sessionFactoryBeanName); bindSimpleValue(identifier.getType().getName(), key, false, columnName, mappings); joinedSubclass.createPrimaryKey(); // properties createClassProperties(sub, joinedSubclass, mappings, sessionFactoryBeanName); } protected String getJoinedSubClassTableName( HibernatePersistentEntity sub, PersistentClass model, Table denormalizedSuperTable, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { String logicalTableName = unqualify(model.getEntityName()); String physicalTableName = getTableName(sub, sessionFactoryBeanName); String schemaName = getSchemaName(mappings); String catalogName = getCatalogName(mappings); mappings.addTableNameBinding(schemaName, catalogName, logicalTableName, physicalTableName, denormalizedSuperTable); return physicalTableName; } /** * Binds a sub-class using table-per-hierarchy inheritance mapping * * @param sub The Grails domain class instance representing the sub-class * @param subClass The Hibernate SubClass instance * @param mappings The mappings instance */ protected void bindSubClass(HibernatePersistentEntity sub, Subclass subClass, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { bindClass(sub, subClass, mappings); if (LOG.isDebugEnabled()) LOG.debug("Mapping subclass: " + subClass.getEntityName() + " -> " + subClass.getTable().getName()); // properties createClassProperties(sub, subClass, mappings, sessionFactoryBeanName); } /** * Creates and binds the discriminator property used in table-per-hierarchy inheritance to * discriminate between sub class instances * * @param table The table to bind onto * @param entity The root class entity * @param mappings The mappings instance */ protected void bindDiscriminatorProperty(Table table, RootClass entity, InFlightMetadataCollector mappings) { Mapping m = getMapping(entity.getMappedClass()); SimpleValue d = new SimpleValue(mappings, table); entity.setDiscriminator(d); DiscriminatorConfig discriminatorConfig = m != null ? m.getDiscriminator() : null; boolean hasDiscriminatorConfig = discriminatorConfig != null; entity.setDiscriminatorValue(hasDiscriminatorConfig ? discriminatorConfig.getValue() : entity.getClassName()); if(hasDiscriminatorConfig) { if (discriminatorConfig.getInsertable() != null) { entity.setDiscriminatorInsertable(discriminatorConfig.getInsertable()); } Object type = discriminatorConfig.getType(); if (type != null) { if(type instanceof Class) { d.setTypeName(((Class)type).getName()); } else { d.setTypeName(type.toString()); } } } if (hasDiscriminatorConfig && discriminatorConfig.getFormula() != null) { Formula formula = new Formula(); formula.setFormula(discriminatorConfig.getFormula()); d.addFormula(formula); } else{ bindSimpleValue(STRING_TYPE, d, false, RootClass.DEFAULT_DISCRIMINATOR_COLUMN_NAME, mappings); ColumnConfig cc = !hasDiscriminatorConfig ? null : discriminatorConfig.getColumn(); if (cc != null) { Column c = (Column) d.getColumnIterator().next(); if (cc.getName() != null) { c.setName(cc.getName()); } bindColumnConfigToColumn(null, c, cc); } } entity.setPolymorphic(true); } protected void configureDerivedProperties(PersistentEntity domainClass, Mapping m) { for (PersistentProperty prop : domainClass.getPersistentProperties()) { PropertyConfig propertyConfig = m.getPropertyConfig(prop.getName()); if (propertyConfig != null && propertyConfig.getFormula() != null) { propertyConfig.setDerived(true); } } } /* * Binds a persistent classes to the table representation and binds the class properties */ protected void bindRootPersistentClassCommonValues(HibernatePersistentEntity domainClass, RootClass root, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { // get the schema and catalog names from the configuration Mapping m = getMapping(domainClass.getJavaClass()); String schema = getSchemaName(mappings); String catalog = getCatalogName(mappings); if (m != null) { configureDerivedProperties(domainClass, m); CacheConfig cc = m.getCache(); if (cc != null && cc.getEnabled()) { root.setCacheConcurrencyStrategy(cc.getUsage()); if ("read-only".equals(cc.getUsage())) { root.setMutable(false); } root.setLazyPropertiesCacheable(!"non-lazy".equals(cc.getInclude())); } Integer bs = m.getBatchSize(); if (bs != null) { root.setBatchSize(bs); } if (m.getDynamicUpdate()) { root.setDynamicUpdate(true); } if (m.getDynamicInsert()) { root.setDynamicInsert(true); } } final boolean hasTableDefinition = m != null && m.getTable() != null; if (hasTableDefinition && m.getTable().getSchema() != null) { schema = m.getTable().getSchema(); } if (hasTableDefinition && m.getTable().getCatalog() != null) { catalog = m.getTable().getCatalog(); } final boolean isAbstract = m != null && !m.getTablePerHierarchy() && m.isTablePerConcreteClass() && root.isAbstract(); // create the table Table table = mappings.addTable(schema, catalog, getTableName(domainClass, sessionFactoryBeanName), null, isAbstract); root.setTable(table); if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Mapping Grails domain class: " + domainClass.getName() + " -> " + root.getTable().getName()); } bindIdentity(domainClass, root, mappings, m, sessionFactoryBeanName); if (m == null) { bindVersion(domainClass.getVersion(), root, mappings, sessionFactoryBeanName); } else { if (m.getVersioned()) { bindVersion(domainClass.getVersion(), root, mappings, sessionFactoryBeanName); } } root.createPrimaryKey(); createClassProperties(domainClass, root, mappings, sessionFactoryBeanName); } protected void bindIdentity(HibernatePersistentEntity domainClass, RootClass root, InFlightMetadataCollector mappings, Mapping gormMapping, String sessionFactoryBeanName) { PersistentProperty identifierProp = domainClass.getIdentity(); if (gormMapping == null) { if(identifierProp != null) { bindSimpleId(identifierProp, root, mappings, null, sessionFactoryBeanName); } return; } Object id = gormMapping.getIdentity(); if (id instanceof CompositeIdentity) { bindCompositeId(domainClass, root, (CompositeIdentity) id, mappings, sessionFactoryBeanName); } else { final Identity identity = (Identity) id; String propertyName = identity.getName(); if (propertyName != null) { PersistentProperty namedIdentityProp = domainClass.getPropertyByName(propertyName); if (namedIdentityProp == null) { throw new MappingException("Mapping specifies an identifier property name that doesn't exist ["+propertyName+"]"); } if (!namedIdentityProp.equals(identifierProp)) { identifierProp = namedIdentityProp; } } bindSimpleId(identifierProp, root, mappings, identity, sessionFactoryBeanName); } } protected void bindCompositeId(PersistentEntity domainClass, RootClass root, CompositeIdentity compositeIdentity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { HibernatePersistentEntity hibernatePersistentEntity = (HibernatePersistentEntity) domainClass; Component id = new Component(mappings, root); id.setNullValue("undefined"); root.setIdentifier(id); root.setEmbeddedIdentifier(true); id.setComponentClassName(domainClass.getName()); id.setKey(true); id.setEmbedded(true); String path = qualify(root.getEntityName(), "id"); id.setRoleName(path); final PersistentProperty[] composite = hibernatePersistentEntity.getCompositeIdentity(); for (PersistentProperty property : composite) { if (property == null) { throw new MappingException("Property referenced in composite-id mapping of class [" + domainClass.getName() + "] is not a valid property!"); } bindComponentProperty(id, null, property, root, "", root.getTable(), mappings, sessionFactoryBeanName); } } /** * Creates and binds the properties for the specified Grails domain class and PersistentClass * and binds them to the Hibernate runtime meta model * * @param domainClass The Grails domain class * @param persistentClass The Hibernate PersistentClass instance * @param mappings The Hibernate Mappings instance * @param sessionFactoryBeanName the session factory bean name */ protected void createClassProperties(HibernatePersistentEntity domainClass, PersistentClass persistentClass, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { final List<PersistentProperty> persistentProperties = domainClass.getPersistentProperties(); Table table = persistentClass.getTable(); Mapping gormMapping = domainClass.getMapping().getMappedForm(); if (gormMapping != null) { table.setComment(gormMapping.getComment()); } List<Embedded> embedded = new ArrayList<>(); for (PersistentProperty currentGrailsProp : persistentProperties) { // if its inherited skip if (currentGrailsProp.isInherited()) { continue; } if(currentGrailsProp.getName().equals(GormProperties.VERSION) ) continue; if (isCompositeIdProperty(gormMapping, currentGrailsProp)) continue; if (isIdentityProperty(gormMapping, currentGrailsProp)) continue; if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Binding persistent property [" + currentGrailsProp.getName() + "]"); } Value value = null; // see if it's a collection type CollectionType collectionType = CT.collectionTypeForClass(currentGrailsProp.getType()); Class<?> userType = getUserType(currentGrailsProp); if (userType != null) { if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); } value = new SimpleValue(mappings, table); bindSimpleValue(currentGrailsProp, null, (SimpleValue) value, EMPTY_PATH, mappings, sessionFactoryBeanName); } else if (collectionType != null) { String typeName = getTypeName(currentGrailsProp, getPropertyConfig(currentGrailsProp),gormMapping); if ("serializable".equals(typeName)) { value = new SimpleValue(mappings, table); bindSimpleValue(typeName, (SimpleValue) value, currentGrailsProp.isNullable(), getColumnNameForPropertyAndPath(currentGrailsProp, EMPTY_PATH, null, sessionFactoryBeanName), mappings); } else { // create collection Collection collection = collectionType.create((ToMany) currentGrailsProp, persistentClass, EMPTY_PATH, mappings, sessionFactoryBeanName); mappings.addCollectionBinding(collection); value = collection; } } else if (currentGrailsProp.getType().isEnum()) { value = new SimpleValue(mappings, table); bindEnumType(currentGrailsProp, (SimpleValue) value, EMPTY_PATH, sessionFactoryBeanName); } else if(currentGrailsProp instanceof Association) { Association association = (Association) currentGrailsProp; if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) { if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as ManyToOne"); value = new ManyToOne(mappings, table); bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, EMPTY_PATH, mappings, sessionFactoryBeanName); } else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.OneToOne && userType == null) { if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as OneToOne"); } final boolean isHasOne = isHasOne(association); if (isHasOne && !association.isBidirectional()) { throw new MappingException("hasOne property [" + currentGrailsProp.getOwner().getName() + "." + currentGrailsProp.getName() + "] is not bidirectional. Specify the other side of the relationship!"); } else if (canBindOneToOneWithSingleColumnAndForeignKey((Association) currentGrailsProp)) { value = new OneToOne(mappings, table, persistentClass); bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne) currentGrailsProp, (OneToOne) value, EMPTY_PATH, sessionFactoryBeanName); } else { if (isHasOne && association.isBidirectional()) { value = new OneToOne(mappings, table, persistentClass); bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne) currentGrailsProp, (OneToOne) value, EMPTY_PATH, sessionFactoryBeanName); } else { value = new ManyToOne(mappings, table); bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, EMPTY_PATH, mappings, sessionFactoryBeanName); } } } else if (currentGrailsProp instanceof Embedded) { embedded.add((Embedded)currentGrailsProp); continue; } } // work out what type of relationship it is and bind value else { if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); } value = new SimpleValue(mappings, table); bindSimpleValue(currentGrailsProp, null, (SimpleValue) value, EMPTY_PATH, mappings, sessionFactoryBeanName); } if (value != null) { Property property = createProperty(value, persistentClass, currentGrailsProp, mappings); persistentClass.addProperty(property); } } for (Embedded association : embedded) { Value value = new Component(mappings, persistentClass); bindComponent((Component) value, association, true, mappings, sessionFactoryBeanName); Property property = createProperty(value, persistentClass, association, mappings); persistentClass.addProperty(property); } bindNaturalIdentifier(table, gormMapping, persistentClass); } private boolean isHasOne(Association association) { return association instanceof org.grails.datastore.mapping.model.types.OneToOne && ((org.grails.datastore.mapping.model.types.OneToOne)association).isForeignKeyInChild(); } protected void bindNaturalIdentifier(Table table, Mapping mapping, PersistentClass persistentClass) { Object o = mapping != null ? mapping.getIdentity() : null; if (!(o instanceof Identity)) { return; } Identity identity = (Identity) o; final NaturalId naturalId = identity.getNatural(); if (naturalId == null || naturalId.getPropertyNames().isEmpty()) { return; } UniqueKey uk = new UniqueKey(); uk.setTable(table); boolean mutable = naturalId.isMutable(); for (String propertyName : naturalId.getPropertyNames()) { Property property = persistentClass.getProperty(propertyName); property.setNaturalIdentifier(true); if (!mutable) property.setUpdateable(false); uk.addColumns(property.getColumnIterator()); } setGeneratedUniqueName(uk); table.addUniqueKey(uk); } protected void setGeneratedUniqueName(UniqueKey uk) { StringBuilder sb = new StringBuilder(uk.getTable().getName()).append('_'); for (Object col : uk.getColumns()) { sb.append(((Column) col).getName()).append('_'); } MessageDigest md; try { md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } try { md.update(sb.toString().getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } String name = "UK" + new BigInteger(1, md.digest()).toString(16); if (name.length() > 30) { // Oracle has a 30-char limit name = name.substring(0, 30); } uk.setName(name); } protected boolean canBindOneToOneWithSingleColumnAndForeignKey(Association currentGrailsProp) { if (currentGrailsProp.isBidirectional()) { final Association otherSide = currentGrailsProp.getInverseSide(); if(otherSide != null) { if (isHasOne(otherSide)) { return false; } if (!currentGrailsProp.isOwningSide() && (otherSide.isOwningSide())) { return true; } } } return false; } protected boolean isIdentityProperty(Mapping gormMapping, PersistentProperty currentGrailsProp) { if (gormMapping == null) { return false; } Object identityMapping = gormMapping.getIdentity(); if (!(identityMapping instanceof Identity)) { return false; } String identityName = ((Identity)identityMapping).getName(); return identityName != null && identityName.equals(currentGrailsProp.getName()); } protected void bindEnumType(PersistentProperty property, SimpleValue simpleValue, String path, String sessionFactoryBeanName) { bindEnumType(property, property.getType(), simpleValue, getColumnNameForPropertyAndPath(property, path, null, sessionFactoryBeanName)); } protected void bindEnumType(PersistentProperty property, Class<?> propertyType, SimpleValue simpleValue, String columnName) { PropertyConfig pc = getPropertyConfig(property); final PersistentEntity owner = property.getOwner(); String typeName = getTypeName(property, getPropertyConfig(property), getMapping(owner)); if (typeName == null) { Properties enumProperties = new Properties(); enumProperties.put(ENUM_CLASS_PROP, propertyType.getName()); String enumType = pc == null ? DEFAULT_ENUM_TYPE : pc.getEnumType(); boolean isDefaultEnumType = enumType.equals(DEFAULT_ENUM_TYPE); simpleValue.setTypeName(ENUM_TYPE_CLASS); if (isDefaultEnumType || "string".equalsIgnoreCase(enumType)) { enumProperties.put(EnumType.TYPE, String.valueOf(Types.VARCHAR)); enumProperties.put(EnumType.NAMED, Boolean.TRUE.toString()); } else if("identity".equals(enumType)) { simpleValue.setTypeName(HibernateUtils.buildIdentityEnumTypeFactory().getName()); } else if (!"ordinal".equalsIgnoreCase(enumType)) { simpleValue.setTypeName(enumType); } simpleValue.setTypeParameters(enumProperties); } else { simpleValue.setTypeName(typeName); } Table t = simpleValue.getTable(); Column column = new Column(); if (owner.isRoot()) { column.setNullable(property.isNullable()); } else { Mapping mapping = getMapping(owner); if (mapping == null || mapping.getTablePerHierarchy()) { if (LOG.isDebugEnabled()) { LOG.debug("[GrailsDomainBinder] Sub class property [" + property.getName() + "] for column name [" + column.getName() + "] set to nullable"); } column.setNullable(true); } else { column.setNullable(property.isNullable()); } } column.setValue(simpleValue); column.setName(columnName); if (t != null) t.addColumn(column); simpleValue.addColumn(column); PropertyConfig propertyConfig = getPropertyConfig(property); if (propertyConfig != null && !propertyConfig.getColumns().isEmpty()) { bindIndex(columnName, column, propertyConfig.getColumns().get(0), t); bindColumnConfigToColumn(property, column, propertyConfig.getColumns().get(0)); } } protected Class<?> getUserType(PersistentProperty currentGrailsProp) { Class<?> userType = null; PropertyConfig config = getPropertyConfig(currentGrailsProp); Object typeObj = config == null ? null : config.getType(); if (typeObj instanceof Class<?>) { userType = (Class<?>)typeObj; } else if (typeObj != null) { String typeName = typeObj.toString(); try { userType = Class.forName(typeName, true, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { // only print a warning if the user type is in a package this excludes basic // types like string, int etc. if (typeName.indexOf(".")>-1) { if (LOG.isWarnEnabled()) { LOG.warn("UserType not found ", e); } } } } return userType; } protected boolean isCompositeIdProperty(Mapping gormMapping, PersistentProperty currentGrailsProp) { if (gormMapping != null && gormMapping.getIdentity() != null) { Object id = gormMapping.getIdentity(); if (id instanceof CompositeIdentity) { String[] propertyNames = ((CompositeIdentity) id).getPropertyNames(); String property = currentGrailsProp.getName(); for (String currentName : propertyNames) { if(currentName != null && currentName.equals(property)) return true; } } } return false; } protected boolean isBidirectionalManyToOne(PersistentProperty currentGrailsProp) { return ((currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) && ((Association)currentGrailsProp).isBidirectional()); } /** * Binds a Hibernate component type using the given GrailsDomainClassProperty instance * * @param component The component to bind * @param property The property * @param isNullable Whether it is nullable or not * @param mappings The Hibernate Mappings object * @param sessionFactoryBeanName the session factory bean name */ protected void bindComponent(Component component, Embedded property, boolean isNullable, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { component.setEmbedded(true); Class<?> type = property.getType(); String role = qualify(type.getName(), property.getName()); component.setRoleName(role); component.setComponentClassName(type.getName()); PersistentEntity domainClass = property.getAssociatedEntity(); evaluateMapping(domainClass, defaultMapping); final List<PersistentProperty> properties = domainClass.getPersistentProperties(); Table table = component.getOwner().getTable(); PersistentClass persistentClass = component.getOwner(); String path = property.getName(); Class<?> propertyType = property.getOwner().getJavaClass(); for (PersistentProperty currentGrailsProp : properties) { if (currentGrailsProp.equals(domainClass.getIdentity())) continue; if (currentGrailsProp.getName().equals(GormProperties.VERSION)) continue; if (currentGrailsProp.getType().equals(propertyType)) { component.setParentProperty(currentGrailsProp.getName()); continue; } bindComponentProperty(component, property, currentGrailsProp, persistentClass, path, table, mappings, sessionFactoryBeanName); } } protected void bindComponentProperty(Component component, PersistentProperty componentProperty, PersistentProperty currentGrailsProp, PersistentClass persistentClass, String path, Table table, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { Value value; // see if it's a collection type CollectionType collectionType = CT.collectionTypeForClass(currentGrailsProp.getType()); if (collectionType != null) { // create collection Collection collection = collectionType.create((ToMany) currentGrailsProp, persistentClass, path, mappings, sessionFactoryBeanName); mappings.addCollectionBinding(collection); value = collection; } // work out what type of relationship it is and bind value else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.ManyToOne) { if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as ManyToOne"); value = new ManyToOne(mappings, table); bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, path, mappings, sessionFactoryBeanName); } else if (currentGrailsProp instanceof org.grails.datastore.mapping.model.types.OneToOne) { if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as OneToOne"); if (canBindOneToOneWithSingleColumnAndForeignKey((Association) currentGrailsProp)) { value = new OneToOne(mappings, table, persistentClass); bindOneToOne((org.grails.datastore.mapping.model.types.OneToOne) currentGrailsProp, (OneToOne) value, path, sessionFactoryBeanName); } else { value = new ManyToOne(mappings, table); bindManyToOne((Association) currentGrailsProp, (ManyToOne) value, path, mappings, sessionFactoryBeanName); } } else if (currentGrailsProp instanceof Embedded) { value = new Component(mappings, persistentClass); bindComponent((Component) value, (Embedded) currentGrailsProp, true, mappings, sessionFactoryBeanName); } else { if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] Binding property [" + currentGrailsProp.getName() + "] as SimpleValue"); value = new SimpleValue(mappings, table); if (currentGrailsProp.getType().isEnum()) { bindEnumType(currentGrailsProp, (SimpleValue) value, path, sessionFactoryBeanName); } else { bindSimpleValue(currentGrailsProp, componentProperty, (SimpleValue) value, path, mappings, sessionFactoryBeanName); } } if (value != null) { Property persistentProperty = createProperty(value, persistentClass, currentGrailsProp, mappings); component.addProperty(persistentProperty); if (isComponentPropertyNullable(componentProperty)) { final Iterator<?> columnIterator = value.getColumnIterator(); while (columnIterator.hasNext()) { Column c = (Column) columnIterator.next(); c.setNullable(true); } } } } protected boolean isComponentPropertyNullable(PersistentProperty componentProperty) { if (componentProperty == null) return false; final PersistentEntity domainClass = componentProperty.getOwner(); final Mapping mapping = getMapping(domainClass.getJavaClass()); return !domainClass.isRoot() && (mapping == null || mapping.isTablePerHierarchy()) || componentProperty.isNullable(); } /* * Creates a persistent class property based on the GrailDomainClassProperty instance. */ protected Property createProperty(Value value, PersistentClass persistentClass, PersistentProperty grailsProperty, InFlightMetadataCollector mappings) { // set type value.setTypeUsingReflection(persistentClass.getClassName(), grailsProperty.getName()); if (value.getTable() != null) { value.createForeignKey(); } Property prop = new Property(); prop.setValue(value); bindProperty(grailsProperty, prop, mappings); return prop; } protected void bindOneToMany(org.grails.datastore.mapping.model.types.OneToMany currentGrailsProp, OneToMany one, InFlightMetadataCollector mappings) { one.setReferencedEntityName(currentGrailsProp.getAssociatedEntity().getName()); one.setIgnoreNotFound(true); } /** * Binds a many-to-one relationship to the * */ @SuppressWarnings("unchecked") protected void bindManyToOne(Association property, ManyToOne manyToOne, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); bindManyToOneValues(property, manyToOne); PersistentEntity refDomainClass = property instanceof ManyToMany ? property.getOwner() : property.getAssociatedEntity(); Mapping mapping = getMapping(refDomainClass); boolean isComposite = hasCompositeIdentifier(mapping); if (isComposite) { CompositeIdentity ci = (CompositeIdentity) mapping.getIdentity(); bindCompositeIdentifierToManyToOne(property, manyToOne, ci, refDomainClass, path, sessionFactoryBeanName); } else { if (property.isCircular() && (property instanceof ManyToMany)) { PropertyConfig pc = getPropertyConfig(property); if (pc.getColumns().isEmpty()) { mapping.getColumns().put(property.getName(), pc); } if (!hasJoinKeyMapping(pc) ) { JoinTable jt = new JoinTable(); final ColumnConfig columnConfig = new ColumnConfig(); columnConfig.setName(namingStrategy.propertyToColumnName(property.getName()) + UNDERSCORE + FOREIGN_KEY_SUFFIX); jt.setKey(columnConfig); pc.setJoinTable(jt); } bindSimpleValue(property, manyToOne, path, pc, sessionFactoryBeanName); } else { // bind column bindSimpleValue(property, null, manyToOne, path, mappings, sessionFactoryBeanName); } } PropertyConfig config = getPropertyConfig(property); if ((property instanceof org.grails.datastore.mapping.model.types.OneToOne) && !isComposite) { manyToOne.setAlternateUniqueKey(true); Column c = getColumnForSimpleValue(manyToOne); if (config != null && !config.isUniqueWithinGroup()) { c.setUnique(config.isUnique()); } else if (property.isBidirectional() && isHasOne(property.getInverseSide())) { c.setUnique(true); } } } protected void bindCompositeIdentifierToManyToOne(Association property, SimpleValue value, CompositeIdentity compositeId, PersistentEntity refDomainClass, String path, String sessionFactoryBeanName) { NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); String[] propertyNames = compositeId.getPropertyNames(); PropertyConfig config = getPropertyConfig(property); List<ColumnConfig> columns = config.getColumns(); int i = columns.size(); int expectedForeignKeyColumnLength = calculateForeignKeyColumnCount(refDomainClass, propertyNames); if (i != expectedForeignKeyColumnLength) { int j = 0; for (String propertyName : propertyNames) { ColumnConfig cc; // if a column configuration exists in the mapping use it if(j < i) { cc = columns.get(j++); } // otherwise create a new one to represent the composite column else { cc = new ColumnConfig(); } // if the name is null then configure the name by convention if(cc.getName() == null) { // use the referenced table name as a prefix String prefix = getTableName(refDomainClass, sessionFactoryBeanName); PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); // if the referenced property is a ToOne and it has a composite id // then a column is needed for each property that forms the composite id if(referencedProperty instanceof ToOne) { ToOne toOne = (ToOne) referencedProperty; PersistentProperty[] compositeIdentity = toOne.getAssociatedEntity().getCompositeIdentity(); if(compositeIdentity != null) { for (PersistentProperty cip : compositeIdentity) { // for each property of a composite id by default we use the table name and the property name as a prefix String compositeIdPrefix = addUnderscore(prefix, namingStrategy.propertyToColumnName(referencedProperty.getName())); String suffix = getDefaultColumnName(cip, sessionFactoryBeanName); String finalColumnName = addUnderscore(compositeIdPrefix, suffix); cc = new ColumnConfig(); cc.setName(finalColumnName); columns.add(cc); } continue; } } String suffix = getDefaultColumnName(referencedProperty, sessionFactoryBeanName); String finalColumnName = addUnderscore(prefix, suffix); cc.setName(finalColumnName); columns.add(cc); } } } bindSimpleValue(property, value, path, config, sessionFactoryBeanName); } // each property may consist of one or many columns (due to composite ids) so in order to get the // number of columns required for a column key we have to perform the calculation here private int calculateForeignKeyColumnCount(PersistentEntity refDomainClass, String[] propertyNames) { int expectedForeignKeyColumnLength = 0; for (String propertyName : propertyNames) { PersistentProperty referencedProperty = refDomainClass.getPropertyByName(propertyName); if(referencedProperty instanceof ToOne) { ToOne toOne = (ToOne) referencedProperty; PersistentProperty[] compositeIdentity = toOne.getAssociatedEntity().getCompositeIdentity(); if(compositeIdentity != null) { expectedForeignKeyColumnLength += compositeIdentity.length; } else { expectedForeignKeyColumnLength++; } } else { expectedForeignKeyColumnLength++; } } return expectedForeignKeyColumnLength; } protected boolean hasCompositeIdentifier(Mapping mapping) { return mapping != null && (mapping.getIdentity() instanceof CompositeIdentity); } protected void bindOneToOne(final org.grails.datastore.mapping.model.types.OneToOne property, OneToOne oneToOne, String path, String sessionFactoryBeanName) { PropertyConfig config = getPropertyConfig(property); final Association otherSide = property.getInverseSide(); final boolean hasOne = isHasOne(otherSide); oneToOne.setConstrained(hasOne); oneToOne.setForeignKeyType(oneToOne.isConstrained() ? ForeignKeyDirection.FROM_PARENT : ForeignKeyDirection.TO_PARENT); oneToOne.setAlternateUniqueKey(true); if (config != null && config.getFetchMode() != null) { oneToOne.setFetchMode(config.getFetchMode()); } else { oneToOne.setFetchMode(FetchMode.DEFAULT); } oneToOne.setReferencedEntityName(otherSide.getOwner().getName()); oneToOne.setPropertyName(property.getName()); oneToOne.setReferenceToPrimaryKey(false); bindOneToOneInternal(property, oneToOne, path); if (hasOne) { PropertyConfig pc = getPropertyConfig(property); bindSimpleValue(property, oneToOne, path, pc, sessionFactoryBeanName); } else { oneToOne.setReferencedPropertyName(otherSide.getName()); } } protected void bindOneToOneInternal(org.grails.datastore.mapping.model.types.OneToOne property, OneToOne oneToOne, String path) { //no-op, for subclasses to extend } /** */ protected void bindManyToOneValues(org.grails.datastore.mapping.model.types.Association property, ManyToOne manyToOne) { PropertyConfig config = getPropertyConfig(property); if (config != null && config.getFetchMode() != null) { manyToOne.setFetchMode(config.getFetchMode()); } else { manyToOne.setFetchMode(FetchMode.DEFAULT); } manyToOne.setLazy(getLaziness(property)); if (config != null) { manyToOne.setIgnoreNotFound(config.getIgnoreNotFound()); } // set referenced entity manyToOne.setReferencedEntityName(property.getAssociatedEntity().getName()); } protected void bindVersion(PersistentProperty version, RootClass entity, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { if(version != null) { SimpleValue val = new SimpleValue(mappings, entity.getTable()); bindSimpleValue(version, null, val, EMPTY_PATH, mappings, sessionFactoryBeanName); if (val.isTypeSpecified()) { if (!(val.getType() instanceof IntegerType || val.getType() instanceof LongType || val.getType() instanceof TimestampType)) { LOG.warn("Invalid version class specified in " + version.getOwner().getName() + "; must be one of [int, Integer, long, Long, Timestamp, Date]. Not mapping the version."); return; } } else { val.setTypeName("version".equals(version.getName()) ? "integer" : "timestamp"); } Property prop = new Property(); prop.setValue(val); bindProperty(version, prop, mappings); prop.setLazy(false); val.setNullValue("undefined"); entity.setVersion(prop); entity.setOptimisticLockStyle(OptimisticLockStyle.VERSION); entity.addProperty(prop); } } @SuppressWarnings("unchecked") protected void bindSimpleId(PersistentProperty identifier, RootClass entity, InFlightMetadataCollector mappings, Identity mappedId, String sessionFactoryBeanName) { Mapping mapping = getMapping(identifier.getOwner()); boolean useSequence = mapping != null && mapping.isTablePerConcreteClass(); // create the id value SimpleValue id = new SimpleValue(mappings, entity.getTable()); // set identifier on entity Properties params = new Properties(); entity.setIdentifier(id); if (mappedId == null) { // configure generator strategy id.setIdentifierGeneratorStrategy(useSequence ? "sequence-identity" : "native"); } else { String generator = mappedId.getGenerator(); if("native".equals(generator) && useSequence) { generator = "sequence-identity"; } id.setIdentifierGeneratorStrategy(generator); params.putAll(mappedId.getParams()); if(params.containsKey(SEQUENCE_KEY)) { params.put(SequenceStyleGenerator.SEQUENCE_PARAM, params.getProperty(SEQUENCE_KEY)); } if ("assigned".equals(generator)) { id.setNullValue("undefined"); } } String schemaName = getSchemaName(mappings); String catalogName = getCatalogName(mappings); params.put(PersistentIdentifierGenerator.IDENTIFIER_NORMALIZER, this.metadataBuildingContext.getObjectNameNormalizer()); if (schemaName != null) { params.setProperty(PersistentIdentifierGenerator.SCHEMA, schemaName); } if (catalogName != null) { params.setProperty(PersistentIdentifierGenerator.CATALOG, catalogName); } id.setIdentifierGeneratorProperties(params); // bind value bindSimpleValue(identifier, null, id, EMPTY_PATH, mappings, sessionFactoryBeanName); // create property Property prop = new Property(); prop.setValue(id); // bind property bindProperty(identifier, prop, mappings); // set identifier property entity.setIdentifierProperty(prop); id.getTable().setIdentifierValue(id); } private String getSchemaName(InFlightMetadataCollector mappings) { Identifier schema = mappings.getDatabase().getDefaultNamespace().getName().getSchema(); if(schema != null) { return schema.getCanonicalName(); } return null; } private String getCatalogName(InFlightMetadataCollector mappings) { Identifier catalog = mappings.getDatabase().getDefaultNamespace().getName().getCatalog(); if(catalog != null) { return catalog.getCanonicalName(); } return null; } /** * Binds a property to Hibernate runtime meta model. Deals with cascade strategy based on the Grails domain model * * @param grailsProperty The grails property instance * @param prop The Hibernate property * @param mappings The Hibernate mappings */ protected void bindProperty(PersistentProperty grailsProperty, Property prop, InFlightMetadataCollector mappings) { // set the property name prop.setName(grailsProperty.getName()); if (isBidirectionalManyToOneWithListMapping(grailsProperty, prop)) { prop.setInsertable(false); prop.setUpdateable(false); } else { prop.setInsertable(getInsertableness(grailsProperty)); prop.setUpdateable(getUpdateableness(grailsProperty)); } AccessType accessType = AccessType.getAccessStrategy( grailsProperty.getMapping().getMappedForm().getAccessType() ); if(accessType == AccessType.FIELD) { EntityReflector.PropertyReader reader = grailsProperty.getReader(); Method getter = reader != null ? reader.getter() : null; if(getter != null && getter.getAnnotation(Traits.Implemented.class) != null) { prop.setPropertyAccessorName(TraitPropertyAccessStrategy.class.getName()); } else { prop.setPropertyAccessorName( accessType.getType() ); } } else { prop.setPropertyAccessorName( accessType.getType() ); } prop.setOptional(grailsProperty.isNullable()); setCascadeBehaviour(grailsProperty, prop); // lazy to true final boolean isToOne = grailsProperty instanceof ToOne; PersistentEntity propertyOwner = grailsProperty.getOwner(); boolean isLazyable = isToOne || !(grailsProperty instanceof Association) && !grailsProperty.equals(propertyOwner.getIdentity()); if (isLazyable) { final boolean isLazy = getLaziness(grailsProperty); prop.setLazy(isLazy); if (isLazy && isToOne && !(PersistentAttributeInterceptable.class.isAssignableFrom(propertyOwner.getJavaClass()))) { handleLazyProxy(propertyOwner, grailsProperty); } } } protected boolean getLaziness(PersistentProperty grailsProperty) { PropertyConfig config = getPropertyConfig(grailsProperty); final Boolean lazy = config.getLazy(); if(lazy == null && grailsProperty instanceof Association) { return true; } else if(lazy != null) { return lazy; } return false; } protected boolean getInsertableness(PersistentProperty grailsProperty) { PropertyConfig config = getPropertyConfig(grailsProperty); return config == null || config.getInsertable(); } protected boolean getUpdateableness(PersistentProperty grailsProperty) { PropertyConfig config = getPropertyConfig(grailsProperty); return config == null || config.getUpdateable(); } protected boolean isBidirectionalManyToOneWithListMapping(PersistentProperty grailsProperty, Property prop) { if(grailsProperty instanceof Association) { Association association = (Association) grailsProperty; Association otherSide = association.getInverseSide(); return association.isBidirectional() && otherSide != null && prop.getValue() instanceof ManyToOne && List.class.isAssignableFrom(otherSide.getType()); } return false; } protected void setCascadeBehaviour(PersistentProperty grailsProperty, Property prop) { String cascadeStrategy = "none"; // set to cascade all for the moment PersistentEntity domainClass = grailsProperty.getOwner(); PropertyConfig config = getPropertyConfig(grailsProperty); if (config != null && config.getCascade() != null) { cascadeStrategy = config.getCascade(); } else if (grailsProperty instanceof Association) { Association association = (Association) grailsProperty; PersistentEntity referenced = association.getAssociatedEntity(); if (isHasOne(association)) { cascadeStrategy = CASCADE_ALL; } else if (association instanceof org.grails.datastore.mapping.model.types.OneToOne) { if (referenced != null && association.isOwningSide()) { cascadeStrategy = CASCADE_ALL; } else { cascadeStrategy = CASCADE_SAVE_UPDATE; } } else if (association instanceof org.grails.datastore.mapping.model.types.OneToMany) { if (referenced != null && association.isOwningSide()) { cascadeStrategy = CASCADE_ALL; } else { cascadeStrategy = CASCADE_SAVE_UPDATE; } } else if (grailsProperty instanceof ManyToMany) { if ((referenced != null && referenced.isOwningEntity(domainClass)) || association.isCircular()) { cascadeStrategy = CASCADE_SAVE_UPDATE; } } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.ManyToOne) { if (referenced != null && referenced.isOwningEntity(domainClass) && !isCircularAssociation(grailsProperty)) { cascadeStrategy = CASCADE_ALL; } else if(isCompositeIdProperty((Mapping) domainClass.getMapping().getMappedForm(), grailsProperty)) { cascadeStrategy = CASCADE_ALL; } else { cascadeStrategy = CASCADE_NONE; } } else if (grailsProperty instanceof Basic) { cascadeStrategy = CASCADE_ALL; } else if (Map.class.isAssignableFrom(grailsProperty.getType())) { referenced = association.getAssociatedEntity(); if (referenced != null && referenced.isOwningEntity(domainClass)) { cascadeStrategy = CASCADE_ALL; } else { cascadeStrategy = CASCADE_SAVE_UPDATE; } } logCascadeMapping(association, cascadeStrategy, referenced); } prop.setCascade(cascadeStrategy); } protected boolean isCircularAssociation(PersistentProperty grailsProperty) { return grailsProperty.getType().equals(grailsProperty.getOwner().getJavaClass()); } protected void logCascadeMapping(Association grailsProperty, String cascadeStrategy, PersistentEntity referenced) { if (LOG.isDebugEnabled() & referenced != null) { String assType = getAssociationDescription(grailsProperty); LOG.debug("Mapping cascade strategy for " + assType + " property " + grailsProperty.getOwner().getName() + "." + grailsProperty.getName() + " referencing type [" + referenced.getJavaClass().getName() + "] -> [CASCADE: " + cascadeStrategy + "]"); } } protected String getAssociationDescription(Association grailsProperty) { String assType = "unknown"; if (grailsProperty instanceof ManyToMany) { assType = "many-to-many"; } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.OneToMany) { assType = "one-to-many"; } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.OneToOne) { assType = "one-to-one"; } else if (grailsProperty instanceof org.grails.datastore.mapping.model.types.ManyToOne) { assType = "many-to-one"; } else if (grailsProperty.isEmbedded()) { assType = "embedded"; } return assType; } /** * Binds a simple value to the Hibernate metamodel. A simple value is * any type within the Hibernate type system * * @param property * @param parentProperty * @param simpleValue The simple value to bind * @param path * @param mappings The Hibernate mappings instance * @param sessionFactoryBeanName the session factory bean name */ protected void bindSimpleValue(PersistentProperty property, PersistentProperty parentProperty, SimpleValue simpleValue, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) { // set type bindSimpleValue(property,parentProperty, simpleValue, path, getPropertyConfig(property), sessionFactoryBeanName); } protected void bindSimpleValue(PersistentProperty grailsProp, SimpleValue simpleValue, String path, PropertyConfig propertyConfig, String sessionFactoryBeanName) { bindSimpleValue(grailsProp, null, simpleValue, path, propertyConfig, sessionFactoryBeanName); } protected void bindSimpleValue(PersistentProperty grailsProp, PersistentProperty parentProperty, SimpleValue simpleValue, String path, PropertyConfig propertyConfig, String sessionFactoryBeanName) { setTypeForPropertyConfig(grailsProp, simpleValue, propertyConfig); final PropertyConfig mappedForm = (PropertyConfig) grailsProp.getMapping().getMappedForm(); if (mappedForm.isDerived() && !(grailsProp instanceof TenantId)) { Formula formula = new Formula(); formula.setFormula(propertyConfig.getFormula()); simpleValue.addFormula(formula); } else { Table table = simpleValue.getTable(); boolean hasConfig = propertyConfig != null; String generator = hasConfig ? propertyConfig.getGenerator() : null; if(generator != null) { simpleValue.setIdentifierGeneratorStrategy(generator); Properties params = propertyConfig.getTypeParams(); if(params != null) { Properties generatorProps = new Properties(); generatorProps.putAll(params); if(generatorProps.containsKey(SEQUENCE_KEY)) { generatorProps.put(SequenceStyleGenerator.SEQUENCE_PARAM, generatorProps.getProperty(SEQUENCE_KEY)); } simpleValue.setIdentifierGeneratorProperties( generatorProps ); } } // Add the column definitions for this value/property. Note that // not all custom mapped properties will have column definitions, // in which case we still need to create a Hibernate column for // this value. List<?> columnDefinitions = hasConfig ? propertyConfig.getColumns() : Arrays.asList(new Object[] { null }); if (columnDefinitions.isEmpty()) { columnDefinitions = Arrays.asList(new Object[] { null }); } for (Object columnDefinition : columnDefinitions) { ColumnConfig cc = (ColumnConfig) columnDefinition; Column column = new Column(); // Check for explicitly mapped column name and SQL type. if (cc != null) { if (cc.getName() != null) { column.setName(cc.getName()); } if (cc.getSqlType() != null) { column.setSqlType(cc.getSqlType()); } } column.setValue(simpleValue); if (cc != null) { if (cc.getLength() != -1) { column.setLength(cc.getLength()); } if (cc.getPrecision() != -1) { column.setPrecision(cc.getPrecision()); } if (cc.getScale() != -1) { column.setScale(cc.getScale()); } if(!mappedForm.isUniqueWithinGroup()) { column.setUnique(cc.isUnique()); } } bindColumn(grailsProp, parentProperty, column, cc, path, table, sessionFactoryBeanName); if (table != null) { table.addColumn(column); } simpleValue.addColumn(column); } } } protected void setTypeForPropertyConfig(PersistentProperty grailsProp, SimpleValue simpleValue, PropertyConfig config) { final String typeName = getTypeName(grailsProp, getPropertyConfig(grailsProp), getMapping(grailsProp.getOwner())); if (typeName == null) { simpleValue.setTypeName(grailsProp.getType().getName()); } else { simpleValue.setTypeName(typeName); if (config != null) { simpleValue.setTypeParameters(config.getTypeParams()); } } } /** * Binds a value for the specified parameters to the meta model. * * @param type The type of the property * @param simpleValue The simple value instance * @param nullable Whether it is nullable * @param columnName The property name * @param mappings The mappings */ protected void bindSimpleValue(String type, SimpleValue simpleValue, boolean nullable, String columnName, InFlightMetadataCollector mappings) { simpleValue.setTypeName(type); Table t = simpleValue.getTable(); Column column = new Column(); column.setNullable(nullable); column.setValue(simpleValue); column.setName(columnName); if (t != null) t.addColumn(column); simpleValue.addColumn(column); } /** * Binds a Column instance to the Hibernate meta model * * @param property The Grails domain class property * @param parentProperty * @param column The column to bind * @param path * @param table The table name * @param sessionFactoryBeanName the session factory bean name */ protected void bindColumn(PersistentProperty property, PersistentProperty parentProperty, Column column, ColumnConfig cc, String path, Table table, String sessionFactoryBeanName) { if (cc != null) { column.setComment(cc.getComment()); column.setDefaultValue(cc.getDefaultValue()); column.setCustomRead(cc.getRead()); column.setCustomWrite(cc.getWrite()); } Class<?> userType = getUserType(property); String columnName = getColumnNameForPropertyAndPath(property, path, cc, sessionFactoryBeanName); if ((property instanceof Association) && userType == null) { Association association = (Association) property; // Only use conventional naming when the column has not been explicitly mapped. if (column.getName() == null) { column.setName(columnName); } if (property instanceof ManyToMany) { column.setNullable(false); } else if (property instanceof org.grails.datastore.mapping.model.types.OneToOne && association.isBidirectional() && !association.isOwningSide()) { if (isHasOne(((Association) property).getInverseSide())) { column.setNullable(false); } else { column.setNullable(true); } } else if ((property instanceof ToOne) && association.isCircular()) { column.setNullable(true); } else { column.setNullable(property.isNullable()); } } else { column.setName(columnName); column.setNullable(property.isNullable() || (parentProperty != null && parentProperty.isNullable())); // Use the constraints for this property to more accurately define // the column's length, precision, and scale if (String.class.isAssignableFrom(property.getType()) || byte[].class.isAssignableFrom(property.getType())) { bindStringColumnConstraints(column, property); } if (Number.class.isAssignableFrom(property.getType())) { bindNumericColumnConstraints(column, property, cc); } } handleUniqueConstraint(property, column, path, table, columnName, sessionFactoryBeanName); bindIndex(columnName, column, cc, table); final PersistentEntity owner = property.getOwner(); if (!owner.isRoot()) { Mapping mapping = getMapping(owner); if (mapping == null || mapping.getTablePerHierarchy()) { if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] Sub class property [" + property.getName() + "] for column name ["+column.getName()+"] set to nullable"); column.setNullable(true); } else { column.setNullable(property.isNullable()); } } if (LOG.isDebugEnabled()) LOG.debug("[GrailsDomainBinder] bound property [" + property.getName() + "] to column name ["+column.getName()+"] in table ["+table.getName()+"]"); } protected void createKeyForProps(PersistentProperty grailsProp, String path, Table table, String columnName, List<?> propertyNames, String sessionFactoryBeanName) { List<Column> keyList = new ArrayList<Column>(); keyList.add(new Column(columnName)); for (Iterator<?> i = propertyNames.iterator(); i.hasNext();) { String propertyName = (String) i.next(); PersistentProperty otherProp = grailsProp.getOwner().getPropertyByName(propertyName); String otherColumnName = getColumnNameForPropertyAndPath(otherProp, path, null, sessionFactoryBeanName); keyList.add(new Column(otherColumnName)); } createUniqueKeyForColumns(table, columnName, keyList); } protected void createUniqueKeyForColumns(Table table, String columnName, List<Column> columns) { Collections.reverse(columns); UniqueKey uk = new UniqueKey(); uk.setTable(table); uk.addColumns(columns.iterator()); if(LOG.isDebugEnabled()) { LOG.debug("create unique key for " + table.getName() + " columns = " + columns); } setGeneratedUniqueName(uk); table.addUniqueKey(uk); } protected void bindIndex(String columnName, Column column, ColumnConfig cc, Table table) { if (cc == null) { return; } Object indexObj = cc.getIndex(); String indexDefinition = null; if (indexObj instanceof Boolean) { Boolean b = (Boolean) indexObj; if (b) { indexDefinition = table.getName() + '_' + columnName + "_idx"; } } else if (indexObj != null) { indexDefinition = indexObj.toString(); } if (indexDefinition == null) { return; } String[] tokens = indexDefinition.split(","); for (int i = 0; i < tokens.length; i++) { String index = tokens[i]; table.getOrCreateIndex(index).addColumn(column); } } protected String getColumnNameForPropertyAndPath(PersistentProperty grailsProp, String path, ColumnConfig cc, String sessionFactoryBeanName) { NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); // First try the column config. String columnName = null; if (cc == null) { // No column config given, so try to fetch it from the mapping PersistentEntity domainClass = grailsProp.getOwner(); Mapping m = getMapping(domainClass); if (m != null) { PropertyConfig c = m.getPropertyConfig(grailsProp.getName()); if (supportsJoinColumnMapping(grailsProp) && hasJoinKeyMapping(c)) { columnName = c.getJoinTable().getKey().getName(); } else if (c != null && c.getColumn() != null) { columnName = c.getColumn(); } } } else { if (supportsJoinColumnMapping(grailsProp)) { PropertyConfig pc = getPropertyConfig(grailsProp); if (hasJoinKeyMapping(pc)) { columnName = pc.getJoinTable().getKey().getName(); } else { columnName = cc.getName(); } } else { columnName = cc.getName(); } } if (columnName == null) { if (isNotEmpty(path)) { columnName = addUnderscore(namingStrategy.propertyToColumnName(path), getDefaultColumnName(grailsProp, sessionFactoryBeanName)); } else { columnName = getDefaultColumnName(grailsProp, sessionFactoryBeanName); } } return columnName; } protected boolean hasJoinKeyMapping(PropertyConfig c) { return c != null && c.getJoinTable() != null && c.getJoinTable().getKey() != null; } protected boolean supportsJoinColumnMapping(PersistentProperty grailsProp) { return grailsProp instanceof ManyToMany || isUnidirectionalOneToMany(grailsProp) || grailsProp instanceof Basic; } protected String getDefaultColumnName(PersistentProperty property, String sessionFactoryBeanName) { NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); String columnName = namingStrategy.propertyToColumnName(property.getName()); if (property instanceof Association) { Association association = (Association) property; boolean isBasic = property instanceof Basic; if(isBasic && ((PropertyConfig)property.getMapping().getMappedForm()).getType() != null ) { return columnName; } if (isBasic) { return getForeignKeyForPropertyDomainClass(property, sessionFactoryBeanName); } if (property instanceof ManyToMany) { return getForeignKeyForPropertyDomainClass(property, sessionFactoryBeanName); } if (!association.isBidirectional() && association instanceof org.grails.datastore.mapping.model.types.OneToMany) { String prefix = namingStrategy.classToTableName(property.getOwner().getName()); return addUnderscore(prefix, columnName) + FOREIGN_KEY_SUFFIX; } if (property.isInherited() && isBidirectionalManyToOne(property)) { return namingStrategy.propertyToColumnName(property.getOwner().getName()) + '_'+ columnName + FOREIGN_KEY_SUFFIX; } return columnName + FOREIGN_KEY_SUFFIX; } return columnName; } protected String getForeignKeyForPropertyDomainClass(PersistentProperty property, String sessionFactoryBeanName) { final String propertyName = NameUtils.decapitalize( property.getOwner().getName() ); NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); return namingStrategy.propertyToColumnName(propertyName) + FOREIGN_KEY_SUFFIX; } protected String getIndexColumnName(PersistentProperty property, String sessionFactoryBeanName) { PropertyConfig pc = getPropertyConfig(property); if (pc != null && pc.getIndexColumn() != null && pc.getIndexColumn().getColumn() != null) { return pc.getIndexColumn().getColumn(); } NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); return namingStrategy.propertyToColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_INDEX_COLUMN_NAME; } protected String getIndexColumnType(PersistentProperty property, String defaultType) { PropertyConfig pc = getPropertyConfig(property); if (pc != null && pc.getIndexColumn() != null && pc.getIndexColumn().getType() != null) { return getTypeName(property, pc.getIndexColumn(), getMapping(property.getOwner())); } return defaultType; } protected String getMapElementName(PersistentProperty property, String sessionFactoryBeanName) { PropertyConfig pc = getPropertyConfig(property); if (hasJoinTableColumnNameMapping(pc)) { return pc.getJoinTable().getColumn().getName(); } NamingStrategy namingStrategy = getNamingStrategy(sessionFactoryBeanName); return namingStrategy.propertyToColumnName(property.getName()) + UNDERSCORE + IndexedCollection.DEFAULT_ELEMENT_COLUMN_NAME; } protected boolean hasJoinTableColumnNameMapping(PropertyConfig pc) { return pc != null && pc.getJoinTable() != null && pc.getJoinTable().getColumn() != null && pc.getJoinTable().getColumn().getName() != null; } /** * Interrogates the specified constraints looking for any constraints that would limit the * length of the property's value. If such constraints exist, this method adjusts the length * of the column accordingly. * @param column the column that corresponds to the property * @param constrainedProperty the property's constraints */ protected void bindStringColumnConstraints(Column column, PersistentProperty constrainedProperty) { final org.grails.datastore.mapping.config.Property mappedForm = constrainedProperty.getMapping().getMappedForm(); Number columnLength = mappedForm.getMaxSize(); List<?> inListValues = mappedForm.getInList(); if (columnLength != null) { column.setLength(columnLength.intValue()); } else if (inListValues != null) { column.setLength(getMaxSize(inListValues)); } } protected void bindNumericColumnConstraints(Column column, PersistentProperty constrainedProperty) { bindNumericColumnConstraints(column, constrainedProperty, null); } /** * Interrogates the specified constraints looking for any constraints that would limit the * precision and/or scale of the property's value. If such constraints exist, this method adjusts * the precision and/or scale of the column accordingly. * @param column the column that corresponds to the property * @param property the property's constraints * @param cc the column configuration */ protected void bindNumericColumnConstraints(Column column, PersistentProperty property, ColumnConfig cc) { int scale = Column.DEFAULT_SCALE; int precision = Column.DEFAULT_PRECISION; PropertyConfig constrainedProperty = (PropertyConfig) property.getMapping().getMappedForm(); if( cc != null && cc.getScale() > - 1) { column.setScale(cc.getScale()); } else if (constrainedProperty.getScale() > -1) { scale = constrainedProperty.getScale(); column.setScale(scale); } if( cc != null && cc.getPrecision() > -1) { column.setPrecision(cc.getPrecision()); } else { Comparable<?> minConstraintValue = constrainedProperty.getMin(); Comparable<?> maxConstraintValue = constrainedProperty.getMax(); int minConstraintValueLength = 0; if ((minConstraintValue != null) && (minConstraintValue instanceof Number)) { minConstraintValueLength = Math.max( countDigits((Number) minConstraintValue), countDigits(((Number) minConstraintValue).longValue()) + scale); } int maxConstraintValueLength = 0; if ((maxConstraintValue != null) && (maxConstraintValue instanceof Number)) { maxConstraintValueLength = Math.max( countDigits((Number) maxConstraintValue), countDigits(((Number) maxConstraintValue).longValue()) + scale); } if (minConstraintValueLength > 0 && maxConstraintValueLength > 0) { // If both of min and max constraints are setted we could use // maximum digits number in it as precision precision = Math.max(minConstraintValueLength, maxConstraintValueLength); } else { // Overwise we should also use default precision precision = DefaultGroovyMethods.max(new Integer[]{precision, minConstraintValueLength, maxConstraintValueLength}); } column.setPrecision(precision); } } /** * @return a count of the digits in the specified number */ protected int countDigits(Number number) { int numDigits = 0; if (number != null) { // Remove everything that's not a digit (e.g., decimal points or signs) String digitsOnly = number.toString().replaceAll("\\D", EMPTY_PATH); numDigits = digitsOnly.length(); } return numDigits; } /** * @return the maximum length of the strings in the specified list */ protected int getMaxSize(List<?> inListValues) { int maxSize = 0; for (Object inListValue : inListValues) { String value = (String) inListValue; maxSize = Math.max(value.length(), maxSize); } return maxSize; } protected void handleLazyProxy(PersistentEntity domainClass, PersistentProperty grailsProperty) { //HibernateUtils.handleLazyProxy(domainClass, grailsProperty); } protected void handleUniqueConstraint(PersistentProperty property, Column column, String path, Table table, String columnName, String sessionFactoryBeanName) { final PropertyConfig mappedForm = (PropertyConfig) property.getMapping().getMappedForm(); if (mappedForm.isUnique()) { if (!mappedForm.isUniqueWithinGroup()) { column.setUnique(true); } else { createKeyForProps(property, path, table, columnName, mappedForm.getUniquenessGroup(), sessionFactoryBeanName); } } } protected boolean isNotEmpty(String s) { return GrailsHibernateUtil.isNotEmpty(s); } protected String qualify(String prefix, String name) { return GrailsHibernateUtil.qualify(prefix, name); } protected String unqualify(String qualifiedName) { return GrailsHibernateUtil.unqualify(qualifiedName); } public MetadataBuildingContext getMetadataBuildingContext() { return metadataBuildingContext; } /** * Second pass class for grails relationships. This is required as all * persistent classes need to be loaded in the first pass and then relationships * established in the second pass compile * * @author Graeme */ class GrailsCollectionSecondPass implements SecondPass { private static final long serialVersionUID = -5540526942092611348L; protected ToMany property; protected InFlightMetadataCollector mappings; protected Collection collection; protected String sessionFactoryBeanName; public GrailsCollectionSecondPass(ToMany property, InFlightMetadataCollector mappings, Collection coll, String sessionFactoryBeanName) { this.property = property; this.mappings = mappings; this.collection = coll; this.sessionFactoryBeanName = sessionFactoryBeanName; } public void doSecondPass(Map<?, ?> persistentClasses, Map<?, ?> inheritedMetas) throws MappingException { bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); createCollectionKeys(); } protected void createCollectionKeys() { collection.createAllKeys(); if (LOG.isDebugEnabled()) { String msg = "Mapped collection key: " + columns(collection.getKey()); if (collection.isIndexed()) msg += ", index: " + columns(((IndexedCollection) collection).getIndex()); if (collection.isOneToMany()) { msg += ", one-to-many: " + ((OneToMany) collection.getElement()).getReferencedEntityName(); } else { msg += ", element: " + columns(collection.getElement()); } LOG.debug(msg); } } protected String columns(Value val) { StringBuilder columns = new StringBuilder(); Iterator<?> iter = val.getColumnIterator(); while (iter.hasNext()) { columns.append(((Selectable) iter.next()).getText()); if (iter.hasNext()) columns.append(", "); } return columns.toString(); } @SuppressWarnings("rawtypes") public void doSecondPass(Map persistentClasses) throws MappingException { bindCollectionSecondPass(property, mappings, persistentClasses, collection, sessionFactoryBeanName); createCollectionKeys(); } } class ListSecondPass extends GrailsCollectionSecondPass { private static final long serialVersionUID = -3024674993774205193L; public ListSecondPass(ToMany property, InFlightMetadataCollector mappings, Collection coll, String sessionFactoryBeanName) { super(property, mappings, coll, sessionFactoryBeanName); } @Override public void doSecondPass(Map<?, ?> persistentClasses, Map<?, ?> inheritedMetas) throws MappingException { bindListSecondPass(property, mappings, persistentClasses, (org.hibernate.mapping.List) collection, sessionFactoryBeanName); } @SuppressWarnings("rawtypes") @Override public void doSecondPass(Map persistentClasses) throws MappingException { bindListSecondPass(property, mappings, persistentClasses, (org.hibernate.mapping.List) collection, sessionFactoryBeanName); } } class MapSecondPass extends GrailsCollectionSecondPass { private static final long serialVersionUID = -3244991685626409031L; public MapSecondPass(ToMany property, InFlightMetadataCollector mappings, Collection coll, String sessionFactoryBeanName) { super(property, mappings, coll, sessionFactoryBeanName); } @Override public void doSecondPass(Map<?, ?> persistentClasses, Map<?, ?> inheritedMetas) throws MappingException { bindMapSecondPass(property, mappings, persistentClasses, (org.hibernate.mapping.Map)collection, sessionFactoryBeanName); } @SuppressWarnings("rawtypes") @Override public void doSecondPass(Map persistentClasses) throws MappingException { bindMapSecondPass(property, mappings, persistentClasses, (org.hibernate.mapping.Map) collection, sessionFactoryBeanName); } } /** * A Collection type, for the moment only Set is supported * * @author Graeme */ static abstract class CollectionType { protected final Class<?> clazz; protected final GrailsDomainBinder binder; protected final MetadataBuildingContext buildingContext; protected static CollectionType SET; protected static CollectionType LIST; protected static CollectionType BAG; protected static CollectionType MAP; protected static boolean initialized; protected static final Map<Class<?>, CollectionType> INSTANCES = new HashMap<Class<?>, CollectionType>(); public abstract Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException; protected CollectionType(Class<?> clazz, GrailsDomainBinder binder) { this.clazz = clazz; this.binder = binder; this.buildingContext = binder.getMetadataBuildingContext(); } @Override public String toString() { return clazz.getName(); } protected void createInstances() { if (initialized) { return; } initialized = true; SET = new CollectionType(Set.class, binder) { @Override public Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { org.hibernate.mapping.Set coll = new org.hibernate.mapping.Set(mappings, owner); coll.setCollectionTable(owner.getTable()); binder.bindCollection(property, coll, owner, mappings, path, sessionFactoryBeanName); return coll; } }; INSTANCES.put(Set.class, SET); INSTANCES.put(SortedSet.class, SET); LIST = new CollectionType(List.class, binder) { @Override public Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { org.hibernate.mapping.List coll = new org.hibernate.mapping.List(mappings, owner); coll.setCollectionTable(owner.getTable()); binder.bindCollection(property, coll, owner, mappings, path, sessionFactoryBeanName); return coll; } }; INSTANCES.put(List.class, LIST); BAG = new CollectionType(java.util.Collection.class, binder) { @Override public Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { Bag coll = new Bag(mappings, owner); coll.setCollectionTable(owner.getTable()); binder.bindCollection(property, coll, owner, mappings, path, sessionFactoryBeanName); return coll; } }; INSTANCES.put(java.util.Collection.class, BAG); MAP = new CollectionType(Map.class, binder) { @Override public Collection create(ToMany property, PersistentClass owner, String path, InFlightMetadataCollector mappings, String sessionFactoryBeanName) throws MappingException { org.hibernate.mapping.Map map = new org.hibernate.mapping.Map(mappings, owner); binder.bindCollection(property, map, owner, mappings, path, sessionFactoryBeanName); return map; } }; INSTANCES.put(Map.class, MAP); } public CollectionType collectionTypeForClass(Class<?> clazz) { createInstances(); return INSTANCES.get(clazz); } } }